← Back to all projects

TYPESCRIPT DEEP DIVE LEARNING PROJECTS

JavaScript was built in 10 days to make monkeys dance on web pages. It wasn't designed for million-line codebases. As applications grew, the lack of static typing became a massive liability—`undefined` is not a function, property 'x' does not exist on type 'y'.

Learn TypeScript: From Zero to Type Wizard

Goal: Master TypeScript deeply—not just as “JavaScript with types,” but as a powerful structural type system. Understand how the compiler works, how type inference flows, how to manipulate types like data, and how to build robust, type-safe architectures that catch bugs before code even runs.


Why TypeScript Matters

JavaScript was built in 10 days to make monkeys dance on web pages. It wasn’t designed for million-line codebases. As applications grew, the lack of static typing became a massive liability—undefined is not a function, property ‘x’ does not exist on type ‘y’.

TypeScript introduces a compile-time type system on top of JavaScript. It allows you to model the shape of your data, ensuring that your code adheres to the contracts you define.

The shift: You stop writing code that checks for errors at runtime, and start writing code that makes errors impossible at compile time.


Core Concept Analysis

1. The Structural Type System

Unlike Java or C# (Nominal Typing), TypeScript uses Structural Typing (Duck Typing). If it walks like a duck and quacks like a duck, it’s a duck—even if you didn’t call it Duck.

    Interface: Duck             Object: RobotDuck
    ┌─────────────┐             ┌─────────────┐
    │  walk()     │             │  walk()     │
    │  quack()    │             │  quack()    │
    └─────────────┘             │  charge()   │ <-- Extra props ok
                                └─────────────┘

    TS: "RobotDuck is assignable to Duck because it has the required shape."

TypeScript Structural Typing - Duck typing concept showing interface compatibility

2. The Type Space vs. Value Space

The most confusing part for beginners. TypeScript code exists in two parallel universes that merge during development but separate at runtime.

      TYPE SPACE (Compile Time)           VALUE SPACE (Runtime)
    ┌───────────────────────────┐       ┌───────────────────────┐
    │                           │       │                       │
    │   interface User {        │       │                       │
    │     id: number;           │       │   const user = {      │
    │     name: string;         │       │     id: 1,            │
    │   }                       │       │     name: "Alice"     │
    │   type ID = User["id"];   │       │   };                  │
    │                           │       │                       │
    │   // Interface gone       │       │   // Interface gone   │
    │   // Type gone            │       │   console.log(user)   │
    │                           │       │                       │
    └─────────────┬─────────────┘       └───────────┬───────────┘
                  │                                 │
                  │   Compiling (tsc)               │
                  └─────────────────────────────────┘
                                  │
                                  ▼
                         JavaScript (No Types)

TypeScript Type Space vs Value Space - Showing how types are erased during compilation

3. Generics: Types as Arguments

Generics allow you to write reusable code components that work with a variety of types rather than a single one. Think of them as function arguments, but for types.

    Function Definition:
    identity<T>(arg: T): T { ... }
             ^       ^    ^
             │       │    └─ Return Type
             │       └─ Argument Type
             └─ Type Parameter (The "Variable")

    Usage:
    identity<string>("Hello")  => T becomes "string"
    identity<number>(42)       => T becomes "number"

TypeScript Generics Anatomy - Function definition with type parameters and usage examples

4. Advanced Type Manipulation

TypeScript lets you program with types. You can loop over them, check conditions, and transform them.

  • Union: A | B (A or B)
  • Intersection: A & B (Combined features of A and B)
  • Mapped Types: Iterating over keys ({ [K in keyof T]: ... })
  • Conditional Types: T extends U ? X : Y (If-statement for types)

5. Control Flow Analysis

TypeScript reads your code like a flow chart to narrow down types.

    var x: string | number;
           │
           ▼
    if (typeof x === "string") {
        │
        └─ TS knows x is "string" here (Narrowing)
           x.toUpperCase(); // ✅ OK
    } else {
        │
        └─ TS knows x is "number" here
           x.toFixed(2);    // ✅ OK
    }

TypeScript Control Flow Analysis - Type narrowing based on runtime checks

6. Type Erasure

It is crucial to understand that types are erased. You cannot check if (x instanceof MyInterface) because MyInterface does not exist in the JavaScript output.


Concept Summary Table

Concept Cluster What You Need to Internalize
Structural Typing Shape matters more than names. Excess properties are allowed in variables but checked in literals.
Type vs. Value interface, type vanish at runtime. class, enum exist in both spaces. You cannot typeof a type at runtime.
Inference TS tries to guess types. Control flow analysis narrows types (e.g., inside if blocks).
Generics Write code that works on any type while still maintaining relationships between inputs and outputs.
Type Guards Runtime checks that tell the compiler “It’s safe to treat this variable as type X now”.
Mapped/Conditional Meta-programming for types. Transforming types based on logic (e.g., “Make all properties optional”).

Deep Dive Reading by Concept

Concept Book & Chapter
Structural Typing Programming TypeScript by Boris Cherny — Ch. 3 (Types)
Generics Effective TypeScript by Dan Vanderkam — Item 26-27 (Generics usage)
Type System Internals TypeScript Deep Dive (Basarat Ali Syed) — Section: Type System
Advanced Types Programming TypeScript by Boris Cherny — Ch. 6 (Advanced Types)
Compiler API TypeScript Deep Dive — Section: Compiler API

Essential Reading Order

  1. Foundation:
    • Programming TypeScript, Ch. 3 & 4 (Types, Functions)
  2. Transformation:
    • Programming TypeScript, Ch. 6 (Advanced Types)
  3. Best Practices:
    • Effective TypeScript, Items 1-30

Project List

We will build projects that force you to use the type system not just to describe code, but to enforce architecture.


Project 1: The “Safe-Fetch” API Wrapper

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: Generics, Interfaces, Async/Await
  • Software or Tool: fetch API (native)
  • Main Book: “Effective TypeScript” by Dan Vanderkam

What you’ll build: A type-safe wrapper around the browser’s fetch API. It will require the user to define the expected response shape before making a request, and it will return a typed Promise.

Why it teaches Generics: You cannot know what an API returns ahead of time. You need a way to say, “I am fetching a User, so treat the result as a User.” This is the quintessential use case for Generics (<T>).

Core challenges you’ll face:

  • Defining a generic function get<T>(url: string): Promise<T> (maps to Generics)
  • Handling standardized error responses vs. success data (maps to Union Types)
  • Creating a unified configuration object (headers, timeouts) (maps to Interfaces)

Key Concepts:

  • Generics: “Programming TypeScript” Ch. 4 - Understanding <T>
  • Promises & Async: “Effective TypeScript” Item 25 - Async functions

Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic JavaScript fetch, understanding of JSON

Real World Outcome

You will create a library file client.ts. When you import this client into another file (e.g., app.ts) and use it, you will see the following behavior in your IDE (VS Code):

  1. Create your interface:
    interface User {
        id: number;
        name: string;
        email: string;
    }
    
  2. Make the call:
    const user = await client.get<User>('https://api.example.com/me');
    
  3. Observe the Magic: When you type user., VS Code will pop up a completion list showing exactly email, id, and name.

    If you try to access a non-existent property:

    console.log(user.isAdmin);
    

    Result: You will see a red squiggly line under isAdmin and the hover text will read: Property 'isAdmin' does not exist on type 'User'.

    This confirms that your network response is now strictly typed, preventing you from assuming fields exist when they don’t.

The Core Question You’re Answering

“How do I bridge the gap between the untyped outside world (APIs) and my typed internal code?”

Most bugs happen at the boundaries of your system. This project teaches you how to strictly guard those boundaries.

Concepts You Must Understand First

Stop and research these before coding:

  1. Generic Functions
    • How do I pass a type as an argument?
    • Book Reference: “Programming TypeScript” Ch. 4
  2. Interfaces vs Types
    • How do I describe the shape of an object?
    • When should I use interface vs type?
  3. Promises
    • What is Promise<T>?
    • Why does async function always return a Promise?

Questions to Guide Your Design

Before implementing, think through these:

  1. Error Handling: How do you represent a failed request? Do you throw an error, or return a Result type (Success Error)?
  2. Defaults: How do you allow the user to override headers while keeping default content-types?
  3. Methods: Will you have separate methods for get, post, put, or one master request method?

Thinking Exercise

function wrapper<T>(data: any): T {
  return data;
}

Questions while tracing:

  • If I call wrapper<string>(123), TypeScript allows it because data is any. How do I prevent this runtime lie? (Hint: You can’t fully at compile time without runtime validation, but you can assert it).
  • What happens if the API returns { "id": "1" } (string) but my interface expects id: number? (Hint: TypeScript is erased at runtime, so it won’t crash… until you do math on it).

The Interview Questions They’ll Ask

  1. “Why use unknown instead of any for API responses?”
  2. “Explain how Generic Constraints (T extends object) work.”
  3. “How do you handle JSON parsing errors safely?”

Hints in Layers

Hint 1: Start with a simple function signature: async function http<T>(request: RequestInfo): Promise<T>. Hint 2: Use the Response body method .json(). Note that .json() returns Promise<any>. You need to cast or assertion this. Hint 3: Create a custom Error class to handle non-200 HTTP status codes.

Books That Will Help

Topic Book Chapter
Generics “Programming TypeScript” Ch. 4
Any vs Unknown “Effective TypeScript” Item 42
Async Functions “Effective TypeScript” Item 25

Project 2: Universal Validator (Runtime Type Checking)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Type Guards, Type Inference, Reflection
  • Software or Tool: None (Pure TS)
  • Main Book: “Programming TypeScript” by Boris Cherny

What you’ll build: A validation library similar to Zod or io-ts. You will build a system where defining a validator automatically infers the TypeScript type. You define the schema once, and get both runtime validation and compile-time types.

Why it teaches Type Inference: This is the “Holy Grail” of TS libraries. You will learn how to use infer keyword, recursive types, and how to carry types through function calls to drive VS Code’s intellisense.

Core challenges you’ll face:

  • Creating a Schema class that knows its own type (maps to Recursive Types)
  • Implementing Infer<typeof schema> logic (maps to Conditional Types & infer)
  • Writing Type Guards that narrow unknown input to the schema type (maps to Type Guards)

Key Concepts:

  • Type Guards: “Programming TypeScript” Ch. 3 - is keyword
  • Type Inference: “TypeScript Deep Dive” - Type Inference
  • Conditional Types: “Programming TypeScript” Ch. 6

Difficulty: Intermediate Time estimate: 1 Week Prerequisites: Project 1, solid understanding of closures

Real World Outcome

You will solve the problem of maintaining duplicate type definitions.

The Experience:

  1. Define your Schema:
    const UserSchema = z.object({
      username: z.string(),
      age: z.number().optional()
    });
    
  2. Extract the Type:
    type User = z.infer<typeof UserSchema>;
    

    If you hover over User in your IDE, you will see: type User = { username: string; age?: number | undefined; }

  3. Validate at Runtime: If you feed it bad data, your library will throw a runtime error.
    try {
        UserSchema.parse({ username: "john", age: "twenty" });
    } catch (e) {
        console.log(e.message);
    }
    

    Console Output:

    Validation Error: Expected number at path 'age', got string.
    

The Core Question You’re Answering

“How can I make a single definition serve as both my runtime validation logic and my compile-time type definition?”

Concepts You Must Understand First

  1. User-Defined Type Guards
    • How does function isString(x: any): x is string work?
  2. The infer keyword
    • How can I extract the return type of a function or the generic argument of a class?
  3. Recursive Types
    • How can a type reference itself (for nested objects)?

Questions to Guide Your Design

  1. Chaining: How do you implement .optional()? It needs to change the internal type from T to T | undefined.
  2. Nesting: How do you handle z.object({ address: z.object({ ... }) })?
  3. Type Extraction: How do you access the generic T stored inside the Validator class from the outside?

Thinking Exercise

type Unwrap<T> = T extends Promise<infer U> ? U : T;

Trace:

  • If T is Promise<string>, infer U captures string. Result: string.
  • If T is number, T extends Promise is false. Result: number.
  • Question: How can you use this pattern to extract the type from Validator<T>?

The Interview Questions They’ll Ask

  1. “What is the difference between interface and type regarding recursion?”
  2. “How do you convert a runtime object literal into a TypeScript type?” (Answer: typeof)
  3. “Explain how infer works in conditional types.”

Hints in Layers

Hint 1: Create a base class Validator<T> that has a parse(input: unknown): T method. Hint 2: The string() factory function should return new Validator<string>(...). Hint 3: For objects, the factory will take a map of validators. The return type is the tricky part—you need a Mapped Type: { [K in keyof Input]: Input[K] extends Validator<infer U> ? U : never }.

Books That Will Help

Topic Book Chapter
Advanced Types “Programming TypeScript” Ch. 6
Type Guards “Effective TypeScript” Item 19
Conditional Types “TypeScript Deep Dive” Conditional Types

Project 3: The “Magic” Event Emitter (Typed Events)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 2: Practical
  • Business Potential: 3. Service & Support
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Mapped Types, Keyof, Tuple Types
  • Software or Tool: Node.js EventEmitter
  • Main Book: “Effective TypeScript” by Dan Vanderkam

What you’ll build: A type-safe Event Emitter where the event name determines the type of the payload arguments. You will prevent users from emitting “userLogin” with a string if it expects a User object.

Why it teaches Mapped Types: You need to map a set of event names (keys) to their expected argument lists (values). You will use keyof, lookup types, and tuple types to enforce strict argument lists.

Core challenges you’ll face:

  • Defining a type map for events (e.g., { login: User, logout: void })
  • Restricting the on and emit methods to only use valid keys from that map (maps to keyof)
  • Typing the arguments as a tuple based on the event key (maps to Tuple Types and Rest Parameters)

Key Concepts:

  • keyof Operator: “Programming TypeScript” Ch. 6
  • Indexed Access Types: T[K] syntax
  • Rest Parameters: ...args: T

Difficulty: Intermediate Time estimate: Weekend Prerequisites: Understanding of Pub/Sub pattern

Real World Outcome

You will create a robust TypedEmitter class. The main outcome is compile-time safety for event-driven architectures.

The Experience:

  1. Setup:
    type AppEvents = {
      'user-login': [string, number]; // username, id
      'error': [Error];
    };
    const bus = new TypedEmitter<AppEvents>();
    
  2. Trying to emit wrong arguments:
    bus.emit('user-login', 'alice'); // Missing argument!
    

    Result: VS Code underlines the line in red. Error Message: Expected 3 arguments, but got 2. (The method + 2 args).

  3. Trying to listen to wrong events:
    bus.on('user-logggggin', () => {}); // Typo!
    

    Result: Red squiggly line. Error Message: Argument of type '"user-logggggin"' is not assignable to parameter of type '"user-login" | "error"'.

The Core Question You’re Answering

“How do I enforce dependencies between function arguments?” (i.e., If arg1 is “A”, arg2 must be “TypeA”).

Concepts You Must Understand First

  1. Lookup Types (T[K])
    • How do I get the type of a property by its name?
  2. Rest parameters with tuples
    • How does (...args: [string, number]) work?
    • Why are tuples distinct from arrays in TS?

Questions to Guide Your Design

  1. Generic Class: The TypedEmitter class needs a Generic parameter <TEvents> to define the map.
  2. Method Signatures: How do you write the on method? on<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): void.
  3. Underlying Implementation: Can you just extend the Node.js EventEmitter and cast it?

Thinking Exercise

type EventMap = { foo: number; bar: string };
type Keys = keyof EventMap; // "foo" | "bar"

Question: How do I write a function that takes a key K and a value V, where V must match EventMap[K]?

The Interview Questions They’ll Ask

  1. “How do you type variable-length arguments in TypeScript?”
  2. “What is the difference between [string, number] (tuple) and (string | number)[] (array)?”
  3. “Why do we need keyof operator?”

Hints in Layers

Hint 1: Define the generic class class TypedEmitter<T extends Record<string, any[]>>. The constraint ensures the map values are arrays (arguments). Hint 2: Use keyof T to restrict the event name argument. Hint 3: Use T[K] to extract the specific argument tuple for that event.

Books That Will Help

Topic Book Chapter
Mapped Types “Programming TypeScript” Ch. 6
Tuples “Effective TypeScript” Item 29

Project 4: Immutable State Store (Redux Clone)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 3: Clever
  • Business Potential: 4. Open Core Infrastructure
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Readonly, DeepReadonly, Recursive Types
  • Software or Tool: React (optional, for demo)
  • Main Book: “Programming TypeScript” by Boris Cherny

What you’ll build: A state management library like Redux or Zustand. The twist: it must enforce Immutability at the type level. You will implement a DeepReadonly type that recursively prevents modification of nested properties, and a produce function (like Immer) that allows safe updates.

Why it teaches Modifiers: You will master readonly, mapped modifiers (-readonly, +readonly), and recursive type definitions. You’ll also learn how to type Higher Order Functions (reducers).

Core challenges you’ll face:

  • creating a DeepReadonly<T> type that drills down into objects and arrays (maps to Recursive Mapped Types)
  • Typing a reducer function (state: T, action: Action) => T
  • Ensuring subscribe listeners get the correct state slice

Key Concepts:

  • Readonly Modifier: “Programming TypeScript” Ch. 6
  • Recursive Types: Defining a type that references itself

Difficulty: Advanced Time estimate: 1-2 Weeks Prerequisites: Project 2, Functional Programming basics

Real World Outcome

You will create a global store where accidental state mutation is flagged as a compile error.

The Experience:

  1. Initialize Store:
    const store = createStore({
        settings: { theme: "dark" }
    });
    
  2. Attempt Mutation:
    const state = store.getState();
    state.settings.theme = "light"; // Direct mutation!
    
  3. The Result: VS Code will underline theme in red. Error Message: Cannot assign to 'theme' because it is a read-only property.

    This proves your DeepReadonly type has successfully drilled down into the nested settings object and locked it.

The Core Question You’re Answering

“How do I use the type system to enforce functional programming paradigms like immutability?”

Concepts You Must Understand First

  1. Mapped Types with Modifiers
    • What does readonly [P in keyof T]: T[P] do?
  2. Recursion in Types
    • How do I handle the case where T[P] is an object itself?
  3. Conditional Types
    • How do I detect if T[P] is an array, object, or primitive?

Questions to Guide Your Design

  1. Arrays: readonly T[] vs ReadonlyArray<T>. How do you make the contents of the array readonly too?
  2. Actions: How do you define a Discriminated Union of actions? type Action = { type: "ADD" } | { type: "REMOVE" }.
  3. Performance: Does deep recursion kill the compiler? (Limit depth if needed).

Thinking Exercise

type ReadonlyPoint = { readonly x: number; readonly y: number };

Question: Can I assign a Point (mutable) to a ReadonlyPoint? (Yes, because ReadonlyPoint is a “wider” type in terms of permissions—it asks for at least read access, which Point provides). Question: Can I assign a ReadonlyPoint to a Point? (No. Point demands write access, which ReadonlyPoint cannot promise).

The Interview Questions They’ll Ask

  1. “Explain how const assertions (as const) differ from readonly properties.”
  2. “How do you remove the readonly modifier from a type using mapped types?” (Answer: -readonly).
  3. “What are the caveats of ReadonlyArray?”

Hints in Layers

Hint 1: DeepReadonly<T> needs to check if T is an object. If so, map over keys and apply DeepReadonly to values. Hint 2: Don’t forget to handle functions! You don’t want to make function properties readonly in a way that breaks calling them (though usually state is plain data). Hint 3: Use T extends (...args: any[]) => any ? T : ... to exclude functions from recursion.

Books That Will Help

Topic Book Chapter
Immutability “Effective TypeScript” Item 17
Mapped Modifiers “Programming TypeScript” Ch. 6
Readonly “Programming TypeScript” Ch. 3

Project 5: The “Hackable” Decorator System (Dependency Injection)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 4: Tech Flex
  • Business Potential: 5. Industry Disruptor (Framework potential)
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Decorators, Metadata Reflection, Class Architecture
  • Software or Tool: reflect-metadata library
  • Main Book: “TypeScript Deep Dive” by Basarat Ali Syed

What you’ll build: A lightweight Dependency Injection (DI) container similar to Angular’s or NestJS’s. You will use decorators like @Service() and @Inject() to automatically wire up class dependencies at runtime using metadata.

Why it teaches Metaprogramming: Decorators are how you modify or annotate classes/methods at design time. You’ll learn about the emitDecoratorMetadata compiler option and how TS stores type information that can be read at runtime.

Core challenges you’ll face:

  • Implementing Class Decorators (@Service) to register classes in a container (maps to Class Decorators)
  • Implementing Parameter Decorators (@Inject) to specify dependencies (maps to Parameter Decorators)
  • Using Reflect.getMetadata to read the types of constructor arguments (maps to Reflection)

Key Concepts:

  • Decorators: Experimental feature (standardized in newer TS versions, but legacy often used for DI).
  • Reflection: Accessing type data at runtime.

Difficulty: Advanced Time estimate: 1 Week Prerequisites: Understanding of OOP and Inversion of Control (IoC)

Real World Outcome

You will create a script main.ts where you never use the word new to instantiate your services.

The Experience (Console Output): When you run ts-node main.ts, you will see:

$ ts-node main.ts

[Container] Registering Service: Logger
[Container] Registering Service: UserService
[Container] Resolving UserService...
[Container]   - Found dependency at index 0: Logger
[Container]   - Resolving Logger...
[Container]   - Logger instantiated.
[Container] UserService instantiated with dependencies [Logger].
LOG: User saved successfully!

You will see the container recursively resolving the dependency graph purely based on the metadata emitted by your decorators.

The Core Question You’re Answering

“How can I write code that inspects itself and manages its own dependencies?”

Concepts You Must Understand First

  1. Decorator Factories
    • Functions that return functions.
  2. tsconfig.json settings
    • You MUST enable experimentalDecorators and emitDecoratorMetadata.
  3. Reflection API
    • What is Reflect.defineMetadata and Reflect.getMetadata?

Questions to Guide Your Design

  1. Singleton vs Transient: How do you store instances? Should @Service({ scope: 'transient' }) create a new instance every time?
  2. Circular Dependencies: What happens if A needs B and B needs A? (Classic DI nightmare).
  3. Testing: How does DI make testing easier? (Mocking).

Thinking Exercise

Trace the execution order:

  1. Decorators run (when file loads).
  2. Constructor runs (when instantiated). In what order are the decorators applied? (Bottom-up, Right-to-left).

The Interview Questions They’ll Ask

  1. “What is the difference between Stage 2 (legacy) decorators and Stage 3 (standard) decorators?”
  2. “How does TypeScript emit type metadata for reflection?”
  3. “What is Inversion of Control?”

Hints in Layers

Hint 1: The container needs a Map<Constructor, Instance>. Hint 2: The @Service decorator should save the class constructor into a registry. Hint 3: The resolve<T>(target: Constructor<T>) method needs to look up design:paramtypes using Reflect.getMetadata.

Books That Will Help

Topic Book Chapter
Decorators “Programming TypeScript” Ch. 8 (Classes) / Appendix
Reflection “TypeScript Deep Dive” Reflection
DI Patterns “Clean Architecture” Dependency Inversion Principle

Project 6: Type-Safe SQL Query Builder (Template Literal Types)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 5. Industry Disruptor
  • Difficulty: Level 5: Master
  • Knowledge Area: Template Literal Types, Conditional Types, infer
  • Software or Tool: SQL (Just strings)
  • Main Book: “Effective TypeScript” by Dan Vanderkam

What you’ll build: A tool that parses SQL strings at compile time and generates the return type of the query. Example: query("SELECT id, name FROM users") -> returns Pick<User, "id" | "name">[].

Why it teaches Template Literal Types: This is the peak of TypeScript’s type system capabilities. You will treat string types as parsable sequences, splitting strings by spaces or commas using infer and recursive conditional types.

Core challenges you’ll face:

  • Parsing a string type: Parse<"SELECT id FROM users"> (maps to Template Literal Types)
  • Splitting strings: Implementing a type-level Split<Str, Delimiter> (maps to Recursive Types)
  • Mapping SQL columns to TS object keys (maps to Mapped Types)

Key Concepts:

  • Template Literal Types: TS 4.1+ feature allowing ${string} manipulation.
  • Intrinsic String Manipulation: Uppercase, Lowercase, etc.

Difficulty: Master Time estimate: 2-3 Weeks Prerequisites: Project 2 & 4, Extreme patience with compiler errors.

Real World Outcome

You write raw SQL strings, and TypeScript understands them better than your database client.

The Experience:

  1. Define DB Schema:
    interface DB {
      users: { id: number; name: string; age: number };
    }
    
  2. Write Query:
    const result = query("SELECT name FROM users");
    
  3. Inspect Result: Hover over result. You will see: const result: { name: string }[]

  4. Try Invalid Access:
    console.log(result[0].age);
    

    Result: VS Code Error: Property 'age' does not exist on type '{ name: string; }'.

    The compiler correctly analyzed that your SQL string only requested the name column, so age is not available at runtime.

The Core Question You’re Answering

“Can the type system parse a specific Domain Specific Language (DSL) inside string literals?”

Concepts You Must Understand First

  1. String Pattern Matching
    • T extends "SELECT ${infer Cols} FROM ${infer Table}" ? ...
  2. Recursive String Parsing
    • How to loop through a comma-separated string type.

Questions to Guide Your Design

  1. Complexity Limit: TypeScript has a recursion limit (around 50-100 levels). How do you keep the parser simple enough not to crash the compiler?
  2. Whitespace: How do you handle SELECT id (double space)? You need a Trim<T> type utility.
  3. Star Select: How do you handle SELECT *? (Return all columns).

Thinking Exercise

type TrimLeft<S extends string> = S extends ` ${infer R}` ? TrimLeft<R> : S;

Trace: TrimLeft<" hello">

  1. Matches ` ` + ` hello. Recurse with hello`.
  2. Matches ` ` + ` hello. Recurse with hello`.
  3. Matches ` ` + hello. Recurse with hello.
  4. No match. Return hello.

The Interview Questions They’ll Ask

  1. “What are intrinsic string manipulation types in TypeScript?”
  2. “How would you create a KebabCase<T> utility type?”
  3. “What are the performance implications of complex recursive types?”

Hints in Layers

Hint 1: Start small. Just parse SELECT * FROM table. Hint 2: Implement Split<S, D> first. S extends ${infer Head}${D}${infer Tail} ? Head | Split<Tail, D> : S. Hint 3: Build a PickColumns utility that takes the DB schema and the union of column names extracted from the parse step.

Books That Will Help

Topic Book Chapter
Template Literals “Learning TypeScript” Strings
Recursive Types “Effective TypeScript” Item 50+ (later editions)

Project 7: The “Strict” CLI (Zod + Inquirer + Discriminated Unions)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 3: Clever
  • Business Potential: 2. Pro Tool
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Discriminated Unions, Control Flow Analysis
  • Software or Tool: Node.js, Commander/Inquirer
  • Main Book: “Programming TypeScript” by Boris Cherny

What you’ll build: A CLI configuration wizard (like npm init) that asks conditional questions. If the user selects “Database: Postgres”, the next question asks for “Port” (number). If they select “SQLite”, it asks for “File Path” (string). The final config object must be strictly typed based on these choices.

Why it teaches Discriminated Unions: You need to model state where the shape of the data changes based on a specific field (the “discriminant”). This is the pattern for handling varied states (Loading vs Success vs Error) or varied configs.

Core challenges you’ll face:

  • Defining a Union type PostgresConfig | SqliteConfig
  • Using the kind or type property to narrow the type (maps to Discriminated Unions)
  • Ensuring the runtime prompts match the compile-time types (maps to Exhaustiveness Checking)

Key Concepts:

  • Discriminated Unions: The “Tag” pattern.
  • Exhaustiveness Checking: Using never to ensure all cases are handled.

Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic CLI experience

Real World Outcome

You will run your CLI and interact with it.

The Experience (Terminal Session):

$ ts-node cli.ts

? Select your database type:
  > Postgres
    SQLite

# User selects Postgres

? Enter Postgres Port: 5432
? Enter Hostname: localhost

# Program Output:
Configuration saved:
{
  "type": "postgres",
  "port": 5432,
  "host": "localhost"
}

The Code Experience: Inside your code, handling the config object becomes strictly typed:

if (config.type === 'sqlite') {
    console.log(config.port); // Error: Property 'port' does not exist on type 'SqliteConfig'.
}

The Core Question You’re Answering

“How do I model data that can be one of several different shapes, and safely distinguish between them?”

Concepts You Must Understand First

  1. The never type
    • How to use it to ensure a switch statement covers all cases.
  2. Union Types
    • Why is A | B less specific than A?

Questions to Guide Your Design

  1. Scalability: If I add MysqlConfig, does the compiler force me to update the wizard logic? (It should).
  2. Validation: How do you validate the user input for “Port” is actually a number?

Thinking Exercise

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

Trace: How does calling this in the default block of a switch statement enforce exhaustiveness? If you forget a case, the default block receives a type that is NOT never, causing a compile error.

The Interview Questions They’ll Ask

  1. “What is a Discriminated Union and why is it preferred over optional properties?”
  2. “What is the purpose of the never type in control flow analysis?”
  3. “How does TypeScript narrow types?”

Hints in Layers

Hint 1: Define the interface for each config type separately, then Union them. Hint 2: Ensure every interface has a common literal property (e.g., type: 'postgres'). Hint 3: In your prompt logic, use a switch on type to decide the next questions.

Books That Will Help

Topic Book Chapter
Unions “Programming TypeScript” Ch. 3
Control Flow “Effective TypeScript” Item 22

Project 8: The Abstract Syntax Tree (AST) Linter

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 4: Tech Flex
  • Business Potential: 3. Service & Support (Custom enterprise rules)
  • Difficulty: Level 4: Expert
  • Knowledge Area: Compiler API, AST, Visitors
  • Software or Tool: typescript (compiler package)
  • Main Book: “TypeScript Deep Dive” (Compiler section)

What you’ll build: A custom linting tool that enforces a specific architectural rule. Example: “Public class methods must return a Promise”. You will not use Regex; you will parse the code into an AST and inspect the nodes.

Why it teaches Compiler Internals: To really master TS, you should understand how it sees your code. The AST (Abstract Syntax Tree) is that representation. You will use the actual TypeScript Compiler API.

Core challenges you’ll face:

  • Setting up the TS Compiler API to parse a file (maps to Compiler API)
  • Traversing the tree (Visitor pattern) (maps to AST Traversal)
  • Identifying specific nodes (e.g., ts.isMethodDeclaration) (maps to Type Guards)

Key Concepts:

  • AST: Tree representation of source code.
  • Scanner/Parser: How text becomes tokens, and tokens become trees.

Difficulty: Expert Time estimate: 1-2 Weeks Prerequisites: Strong recursion skills

Real World Outcome

You will run your linter against a sample file with known “bad” code.

The Experience (Terminal Output):

$ node my-linter.js ./src/legacy-code.ts

Linting ./src/legacy-code.ts...

[Error] Line 15, Column 4:
    public getUser(id: number): User {
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Rule Violation: All public methods in this project must return a Promise<T>.
    Reason: We are migrating to an async architecture.

Found 1 error.

The Core Question You’re Answering

“How does TypeScript actually understand my code?”

Concepts You Must Understand First

  1. Tree Data Structures
    • Nodes, Children, Parents.
  2. Visitor Pattern
    • Walking a tree.
  3. Compiler Phases
    • Scanner -> Parser -> Binder -> Checker -> Emitter.

Questions to Guide Your Design

  1. Scope: How do you distinguish between a public method and a private one? (Check modifiers array on the node).
  2. Types: Do you need the Type Checker, or just the AST? (Just AST for syntax checks, Checker for type checks).

Thinking Exercise

Explore the TypeScript AST Viewer. Paste some code and see the tree. Task: Find the node type for export class Foo {}. It is a ClassDeclaration with a modifiers array containing ExportKeyword.

The Interview Questions They’ll Ask

  1. “What is the difference between the AST and the Symbol Table?”
  2. “How does TypeScript resolve module imports internally?”
  3. “What is a SourceFile in TS API?”

Hints in Layers

Hint 1: Use ts.createSourceFile to parse text into an AST. Hint 2: Write a recursive function visit(node: ts.Node) that calls ts.forEachChild(node, visit). Hint 3: Inside visit, check ts.isMethodDeclaration(node).

Books That Will Help

Topic Book Chapter
Compiler API “TypeScript Deep Dive” Compiler
Visitors “Design Patterns” (GoF) Visitor

Project 9: Reactivity System (Proxies & Reflect)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 4: Tech Flex
  • Business Potential: 5. Industry Disruptor (Framework)
  • Difficulty: Level 4: Expert
  • Knowledge Area: Proxies, Generics, Recursive Types
  • Software or Tool: JS Proxy API
  • Main Book: “Effective TypeScript” by Dan Vanderkam

What you’ll build: A fine-grained reactivity system like Vue 3’s reactive() or SolidJS’s signals. You will wrap objects in ES6 Proxies to intercept reads/writes, track dependencies, and trigger effects. Crucially, the proxy must be fully typed—reactive<T>(obj: T): T—maintaining deep property access types.

Why it teaches Proxies & Types: Proxies are powerful but notoriously hard to type correctly because they change runtime behavior. You’ll learn to model “transparent” wrappers that maintain original types while adding side effects.

Core challenges you’ll face:

  • Typing the Proxy handler to ensure strict adherence to the target object’s shape
  • Handling deep reactivity (wrapping nested objects recursively)
  • Typing the effect(() => void) system

Key Concepts:

  • ES6 Proxies: Intercepting operations.
  • Recursive Types: Handling nested objects.

Difficulty: Expert Time estimate: 1 Week Prerequisites: Deep understanding of JS runtime

Real World Outcome

You will see your “magic” variable updates triggering functions automatically in the console.

The Experience (Console Output):

const state = reactive({ count: 0 });

effect(() => {
  console.log(`[Effect] The count is now: ${state.count}`);
});

console.log("Incrementing...");
state.count++;

console.log("Incrementing again...");
state.count++;

Output:

[Effect] The count is now: 0
Incrementing...
[Effect] The count is now: 1
Incrementing again...
[Effect] The count is now: 2

The Core Question You’re Answering

“How do I intercept property access while keeping the compiler happy and unaware that interception is happening?”

Concepts You Must Understand First

  1. Meta-programming with Proxy
    • Traps (get, set).
    • Reflect API.

Questions to Guide Your Design

  1. Unwrapping: If I pass a reactive object to a function expecting a normal object, does it work? (Yes, if typed as T).
  2. Identity: Is state === originalObject? (No).
  3. Memory Leaks: How do you clean up effects?

Thinking Exercise

const p = new Proxy(target, handler);

Question: How do I tell TypeScript that p is the same type as target? You simply assert it: return new Proxy(...) as T.

The Interview Questions They’ll Ask

  1. “What are the limitations of TypeScript when working with Proxies?”
  2. “How do you type a Proxy that adds dynamic properties?”
  3. “What is the difference between Proxy and Object.defineProperty?”

Hints in Layers

Hint 1: The return type of reactive<T> is just T. The proxy is transparent. Hint 2: In the get trap, if the result is an object, wrap it in reactive before returning (lazy recursion). Hint 3: Use a global stack to track the “currently running effect”.

Books That Will Help

Topic Book Chapter
Proxies “JavaScript: The Definitive Guide” Meta-programming
Reactivity “Vue.js 3 Internals” Reactivity

Project 10: The Ultimate Monorepo (Workspaces & Config)

  • File: TYPESCRIPT_DEEP_DIVE_LEARNING_PROJECTS.md
  • Main Programming Language: TypeScript
  • Coolness Level: Level 2: Practical
  • Business Potential: 3. Service & Support
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Build Systems, Module Resolution, Project References
  • Software or Tool: Yarn/PNPM Workspaces, TurboRepo (optional)
  • Main Book: “TypeScript Deep Dive” (Configuration)

What you’ll build: A complex project structure with 3 packages: core (shared logic), ui (shared components), and app (consumes both). You will configure TypeScript Project References (composite: true) so that building app automatically checks/builds dependencies, and Go to Definition works across packages.

Why it teaches Architecture: TypeScript isn’t just about code; it’s about compilation context. Understanding tsconfig.json, paths, rootDirs, and references is vital for scaling TS to large codebases.

Core challenges you’ll face:

  • Configuring paths aliases (e.g., @my/core) vs package.json exports
  • Setting up Incremental Builds (tsbuildinfo)
  • Understanding moduleResolution: node vs bundler

Key Concepts:

  • Project References: Breaking a large compilation into smaller chunks.
  • Declaration Maps: Jumping to source instead of .d.ts.

Difficulty: Advanced Time estimate: 1 Week Prerequisites: NPM/Yarn experience

Real World Outcome

You will run a build command that proves TypeScript is respecting your dependency graph.

The Experience (Terminal Output): When you run the build from the root:

$ tsc -b --verbose

[10:00:01] Projects in this build:
    * packages/core/tsconfig.json
    * packages/ui/tsconfig.json
    * packages/app/tsconfig.json

[10:00:02] Building packages/core/tsconfig.json...
[10:00:03] Building packages/ui/tsconfig.json...
[10:00:04] Building packages/app/tsconfig.json...
[Success] Build completed in 3.4s

Then, if you modify a file in ui and run it again:

$ tsc -b --verbose

[10:05:01] Project 'packages/core/tsconfig.json' is up to date because newest input '...' is older than output '...'
[10:05:01] Building packages/ui/tsconfig.json...
[10:05:02] Building packages/app/tsconfig.json...

You see Incremental Compilation in action—Core was skipped!

The Core Question You’re Answering

“How do I scale TypeScript compilation so it doesn’t take 5 minutes to build?”

Concepts You Must Understand First

  1. Module Resolution Strategies
    • Classic vs Node vs Node16.
  2. Path Mapping
    • How paths in tsconfig works (and why it doesn’t affect runtime!).

Questions to Guide Your Design

  1. Circular Deps: Project references strictly forbid cycles. How do you architect your code to avoid them?
  2. Output: Where do the .js files go? (Separate dist folders or alongside src?).

Thinking Exercise

Open a massive node_modules folder. Look at the .d.ts files. Question: How does VS Code know that import "react" refers to that specific file? (It follows package.json -> types field).

The Interview Questions They’ll Ask

  1. “What is the purpose of declarationMap in tsconfig?”
  2. “Explain TypeScript Project References.”
  3. “What is the difference between tsc and tsc --build?”

Hints in Layers

Hint 1: Every package needs its own tsconfig.json. Hint 2: The root tsconfig.json should act as a solution file with files: [] and references: [...]. Hint 3: Use tsc -b (build mode) instead of just tsc.

Books That Will Help

Topic Book Chapter
Project Refs TS Official Docs Project References
Monorepos “Micro-Frontends in Action” Code Sharing

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
Safe-Fetch Beginner Weekend ⭐⭐ ⭐⭐
Universal Validator Intermediate 1 Week ⭐⭐⭐⭐ ⭐⭐⭐⭐
Typed Emitter Intermediate Weekend ⭐⭐⭐ ⭐⭐
Immutable Store Advanced 1-2 Wks ⭐⭐⭐⭐ ⭐⭐⭐
Decorator DI Advanced 1 Week ⭐⭐⭐ ⭐⭐⭐⭐⭐
SQL Builder Master 2-3 Wks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Strict CLI Intermediate Weekend ⭐⭐ ⭐⭐⭐
AST Linter Expert 2 Wks ⭐⭐⭐⭐⭐ ⭐⭐⭐
Reactivity Expert 1 Week ⭐⭐⭐⭐ ⭐⭐⭐⭐
Monorepo Advanced 1 Week ⭐⭐⭐

Recommendation

  • For the Pragmatist: Start with Project 1 (Safe-Fetch) and Project 7 (Strict CLI). These give immediate ROI in daily work.
  • For the Library Author: Do Project 2 (Universal Validator) and Project 6 (SQL Builder). These teach you how to write types for others.
  • For the Architect: Focus on Project 5 (DI) and Project 10 (Monorepo). This is how you structure systems.

Final Overall Project: The “Full-Stack” Type-Safe Framework

What you’ll build: A mini-framework that combines your SQL Builder, Validator, DI Container, and Router. You will create an API where you define a database schema and a validator, and the framework automatically generates:

  1. The SQL queries (checked at compile time).
  2. The API Routes (with validation).
  3. The Client SDK (fully typed).

The Goal: Change a database column name, and watch the frontend API call turn red in VS Code immediately. End-to-end type safety from DB to UI.


Summary

This learning path covers TypeScript through 10 hands-on projects.

# Project Name Main Language Difficulty Time Estimate
1 Safe-Fetch API Wrapper TypeScript Beginner Weekend
2 Universal Validator TypeScript Intermediate 1 Week
3 Typed Event Emitter TypeScript Intermediate Weekend
4 Immutable State Store TypeScript Advanced 1-2 Wks
5 DI Container TypeScript Advanced 1 Week
6 SQL Query Builder TypeScript Master 2-3 Wks
7 Strict CLI TypeScript Intermediate Weekend
8 AST Linter TypeScript Expert 2 Wks
9 Reactivity System TypeScript Expert 1 Week
10 The Ultimate Monorepo TypeScript Advanced 1 Week

Expected Outcomes

After completing these, you will:

  • Stop fighting the compiler and start making it work for you.
  • Understand generic inference flows deeply.
  • Be able to read and write complex library definitions (.d.ts).
  • Know how to configure TS for any environment.
  • Write code that eliminates entire classes of runtime bugs.