Why I rebuilt this from the ground up
Most engineers learn TypeScript backwards: you inherit a codebase, copy an interface pattern you don't fully understand, and move on the moment the red squiggly line disappears. That works until it doesn't — until you're staring at a generic constraint error or a mapped type that refuses to narrow, with no mental model to reach for.
So I did the opposite. I built a dedicated repo, numbered it like a syllabus, and worked through the type system deliberately: basics, the compiler, classes and interfaces, advanced types, generics, type derivation (keyof, typeof, infer, conditional and mapped types), decorators, modules, and then the tooling layer — Vite, Webpack, and a React integration — on top. Each folder is a self-contained lesson with its own tsconfig.json, so I could isolate compiler behavior without one module's config leaking into another's.
This isn't a demo app pretending to be a "project." It's closer to a lab notebook — and that's the point. Clients and teams don't need proof that I can wire up a CRUD app; there are a hundred of those in my portfolio already. What they need is confidence that when TypeScript's type system gets adversarial — discriminated unions, conditional types, infer — I'm not guessing.
The type system, not just the syntax
The early folders (02. Basics through 07. Advanced Types) cover the vocabulary everyone knows on paper — unions, tuples, enums, interfaces — but I pushed into the parts that actually separate "writes TypeScript" from "thinks in TypeScript": discriminated unions and type guards.
type FileSource = { type: 'file'; path: string };
type DBSource = { type: 'db'; connectionUrl: string };
type Source = FileSource | DBSource;
function loadData(source: Source) {
if (source.type === 'file') {
source.path; // narrowed to FileSource
return;
}
source.connectionUrl; // narrowed to DBSource
}
The type tag is what makes this exhaustive and safe — no as casts, no any escape hatches. I also worked through the instanceof-based guard pattern for class hierarchies (User vs Admin), because discriminated unions only work when you control the shape; runtime class checks are the fallback when you don't.
Deriving types instead of writing them twice
The 10. Deriving Types folder is where the repo earns its keep. This is the part of TypeScript most engineers avoid — conditional types, infer, mapped types, template literal types — because the payoff isn't obvious until you've been burned by keeping two type definitions in sync by hand.
Conditional types with infer, used to extract an array's element type generically:
type GetElementType<T> = T extends any[] ? T[number] : never;
type Example1 = GetElementType<string[]>; // string
type Example2 = GetElementType<string>; // never
Mapped types, used to derive a "results" shape from an existing operations object without redeclaring it:
type Operations = {
add: (a: number, b: number) => number;
subtract: (a: number, b: number) => number;
};
type Results<T> = {
readonly [Key in keyof T]?: number;
};
let mathResults: Results<Operations> = {
add: mathOperations.add(1, 2),
subtract: mathOperations.subtract(5, 2),
};
And template literal types, combined with mapped types to generate event-name unions from a data shape:
type ReadPermissions = 'no-read' | 'read';
type WritePermissions = 'no-write' | 'write';
type FilePermissions = `${ReadPermissions}-${WritePermissions}`;
type DataFileEventNames = `${keyof DataFile}Changed`;
type DataFileEvents = {
[Key in DataFileEventNames]: () => void;
};
This is the pattern I now reach for whenever I see two hand-written types that are really the same information expressed twice — a request DTO and its "changed" event map, a config object and its partial/readonly variants. Derive one from the other and the compiler keeps them honest.
Generics as a design tool, not decoration
The 09. Demo and 08. Generic Types folders work through generics on a real data structure — a singly linked list — rather than toy identity<T>(x: T) examples that don't reveal anything about variance or constraint design.
class ListNode<T> {
next?: ListNode<T>;
constructor(public value: T) {}
}
class LinkedList<T> {
private root?: ListNode<T>;
private tail?: ListNode<T>;
private length = 0;
add(value: T) {
const node = new ListNode(value);
if (!this.root || !this.tail) {
this.root = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.length++;
}
// insertAt, removeAt, print, getNumberOfElements...
}
Implementing insertAt and removeAt with generics forces you to confront the actual reason generics exist: LinkedList<number> and LinkedList<string> should share one implementation with zero any, and the compiler should still catch a numberList.add('oops') at the call site. Toy examples don't test that; a real structure with pointer manipulation does.
Tooling: three ways to compile the same TypeScript
The back half of the repo (14–19) is deliberately about the build pipeline, because "I know TypeScript" is incomplete without "I know how TypeScript gets from source to something a browser runs." I set up the same kind of project three different ways:
| Approach | Folder | What it's for |
|---|---|---|
| Native ES modules | 14. Modules | No bundler — browser-native import/export, understanding what the platform gives you for free |
| Vite | 15. Vite | Fast dev server, ESM-native dev experience, minimal config |
| Webpack | 16. Webpack | Full manual control over loaders, dev server, and source maps |
The Webpack config is intentionally explicit rather than relying on presets:
module.exports = {
mode: 'development',
entry: './src/app.ts',
devServer: { static: [{ directory: path.join(__dirname) }] },
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
},
devtool: 'inline-source-map',
module: {
rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }],
},
resolve: { extensions: ['.ts', '.js'] },
};
Writing this by hand instead of using create-react-app or a Vite template means I know exactly what ts-loader is doing, why resolve.extensions matters for extensionless imports, and what breaks if devtool isn't set correctly for production. Folder 19. React then layers a Vite + React + TypeScript setup on top, so the same generic and narrowing patterns from earlier folders carry into component props and event handlers instead of staying academic.
What this demonstrates
A repo like this isn't meant to impress with scale — it's meant to prove depth. When I take on frontend or full-stack work, the value isn't that I can import a UI library; it's that when a generic constraint fights me, or a third-party type definition is wrong, or a build needs a custom Webpack loader instead of a template's defaults, I've already done the work of understanding why, not just how. That's the difference between code that passes review and code that holds up six months later when someone else has to extend it.