TypeScript ULTIMATE Deep-Dive
A complete learning path and practical engineering guide: Beginner → Expert → Compiler-Minded Architect
1. Big Picture
What TypeScript Actually Is
TypeScript is a static type system layered on top of JavaScript. It is NOT a new language — it is JavaScript with a compile-time type layer that is completely erased before execution. Every valid JavaScript program is a valid TypeScript program. TypeScript adds zero runtime behavior.
Why this matters: TypeScript doesn’t “run.” It checks, then emits JavaScript. The types you write are a parallel universe that exists only during development and compilation. At runtime, there is no interface, no type, no generic — just plain JavaScript.
Why TypeScript Exists
JavaScript was designed for 10-line scripts in 1995. Modern applications are 500K+ LOC monorepos with hundreds of engineers. TypeScript exists to:
- Catch errors before runtime — shift bugs left
- Enable tooling — autocomplete, refactoring, navigation
- Serve as documentation — types ARE the contract
- Enable large-scale collaboration — enforce API boundaries
- Reduce cognitive load — the compiler remembers what you forget
Core Concepts
JavaScript Runtime vs TypeScript Compile-Time
┌─────────────────────────────────────────────────┐
│ COMPILE TIME │
│ ┌───────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Source │───>│ Parser │───>│ AST │ │
│ │ .ts/.tsx │ └───────────┘ └────┬─────┘ │
│ └───────────┘ │ │
│ ┌─────▼─────┐ │
│ │ Type │ │
│ │ Checker │ │
│ └─────┬─────┘ │
│ ┌─────▼─────┐ │
│ │ Emitter │ │
│ └─────┬─────┘ │
│ │ │
│ Types exist HERE ─────────────────────│ │
│ Types are ERASED at the boundary ──────┤ │
└─────────────────────────────────────────┼───────┘
│
┌─────────────────────────────────────────▼───────┐
│ RUNTIME │
│ ┌───────────┐ │
│ │ .js │ ← No types. Just JavaScript. │
│ │ output │ │
│ └───────────┘ │
│ │
│ V8 / SpiderMonkey / JavaScriptCore │
└──────────────────────────────────────────────────┘
Key Terminology
| Term | Definition |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------------- |
| Transpilation | Converting TS → JS. Types are stripped, syntax may be downleveled (e.g., enum → IIFE). |
| Type checking | Verifying that values conform to their declared/inferred types. Separate from emit. |
| Type inference | The compiler deduces types without explicit annotations. TS has one of the most powerful inference engines of any mainstream language. |
| Structural typing | Types are compatible if their shapes match, not their names. A Dog with {name: string} is assignable to {name: string} even without inheritance. |
| Nominal typing | Types are compatible only if they share the same declaration. TypeScript does NOT have this natively (but you can simulate it with branded types). |
| Type erasure | All type annotations, interfaces, type aliases, generics — everything type-level — is removed during emit. Zero runtime cost. |
| AST | Abstract Syntax Tree — the tree data structure the parser produces from source code. Every node represents a syntactic construct. |
| Declaration files | .d.ts files — type-only descriptions of JS modules. They are the public API contract of a library. |
| Module resolution | How TS finds the file/types for import x from "y". Complex rules involving node, node16, bundler strategies. |
| Control flow analysis | The compiler tracks variable types through if, switch, assignments, etc. This is how narrowing works. |
| Narrowing | Refining a broad type to a more specific one via control flow (typeof, instanceof, discriminants, in, truthiness). |
| Generics | Type parameters — functions/types that work over a range of types rather than a single one. The backbone of reusable type-safe code. |
| Distributive conditional types | T extends U ? X : Y distributes over union members when T is a naked type parameter. string | numberbecomes(string extends U ? X : Y) | (number extends U ? X : Y). |
Compiler Pipeline
Source Code (.ts / .tsx)
│
▼
┌──────────────┐
│ Scanner │ Tokenizes source into a stream of tokens
└──────┬───────┘
▼
┌──────────────┐
│ Parser │ Builds AST from tokens
└──────┬───────┘
▼
┌──────────────┐
│ Binder │ Creates symbols, sets up scopes, connects declarations
└──────┬───────┘
▼
┌──────────────┐
│ Type Checker │ Resolves types, checks assignability, reports diagnostics
└──────┬───────┘
▼
┌──────────────┐
│ Emitter │ Produces .js, .d.ts, .js.map outputs
└──────────────┘
Key insight: The Binder is the step most people don’t know about. It creates the symbol table — the mapping from names to declarations. Without it, the type checker can’t resolve anything.
Type System Comparisons
| Aspect | TypeScript | Flow | Rust | Go | Java/C# |
|---|---|---|---|---|---|
| Typing | Structural | Structural | Nominal + traits | Structural (interfaces) | Nominal |
| Soundness | Intentionally unsound | More sound | Sound | Sound | Mostly sound |
| Inference | Powerful, bidirectional | Strong | Powerful | Minimal | Limited |
| Generics | Yes, type-erased | Yes | Yes, monomorphized | Yes (since 1.18) | Yes, reified (C#) / erased (Java) |
| Null safety | strictNullChecks | Yes | Option<T> | Nil checks | Nullable annotations |
| Runtime types | No (erased) | No (erased) | Yes | Yes | Yes (reflection) |
| Philosophy | Pragmatic DX | Correctness | Safety + performance | Simplicity | Enterprise OOP |
Why TypeScript is intentionally unsound:
- Soundness would break too much real-world JS code
- Examples of intentional unsoundness: bivariant function parameters (in methods),
any, type assertions,enumvalue access - Trade-off: ergonomics and adoption over mathematical purity
When TypeScript Helps vs Hurts
Helps:
- Large codebases (>10K LOC)
- Team collaboration
- Refactoring confidence
- API contract enforcement
- Tooling (IDE autocomplete, go-to-definition)
- Onboarding new engineers
Becomes dangerous when:
- Types lie (assertions,
anyleaks, unsound patterns) - Over-engineered type gymnastics that no one can read
- Runtime validation is skipped because “TypeScript handles it”
- Build times explode from complex conditional types
- Types create a false sense of security at system boundaries (API, DB, user input)
Overengineered typing signals:
- You need more than 3 levels of nested conditional types
- Reading a type requires more effort than reading the implementation
as anyis used to silence errors caused by your own types- Compile times degrade noticeably
- New team members can’t understand your types within 10 minutes
2. Learning Roadmap by Skill Level
Level 1 — Newbie
Core Concepts
// Primitive types
let name: string = 'Alice';
let age: number = 30;
let active: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
let id: bigint = 100n;
let sym: symbol = Symbol('id');
// Type inference — let TS figure it out
let city = 'Berlin'; // inferred as string
const country = 'DE'; // inferred as "DE" (literal type!)
// WHY: `let` widens, `const` narrows. The compiler knows a const
// can never change, so it infers the narrowest possible type.
// Objects
type User = {
name: string;
age: number;
email?: string; // optional
};
// Interface — almost identical, but extendable via declaration merging
interface Product {
id: string;
price: number;
}
// WHY use type vs interface?
// - `type` for unions, intersections, mapped types, primitives
// - `interface` for object shapes you might extend or merge
// - In practice: pick one convention and be consistent
// - Performance: interfaces are slightly faster for the compiler
// in large codebases (they are cached by name)
// Arrays
const nums: number[] = [1, 2, 3];
const names: Array<string> = ['a', 'b']; // generic syntax
const tuple: [string, number] = ['age', 30]; // fixed-length typed array
// Functions
function greet(name: string): string {
return `Hello, ${name}`;
}
// Arrow function with inferred return
const add = (a: number, b: number) => a + b;
// Void — function returns nothing
function log(msg: string): void {
console.log(msg);
}
// Union types
type Status = 'loading' | 'success' | 'error';
function handle(status: Status) {
// Narrowing via control flow
if (status === 'loading') {
// status: "loading"
} else if (status === 'success') {
// status: "success"
} else {
// status: "error"
}
}
// typeof narrowing
function process(input: string | number) {
if (typeof input === 'string') {
return input.toUpperCase(); // narrowed to string
}
return input.toFixed(2); // narrowed to number
}
Common Beginner Mistakes
- Using
anyeverywhere — defeats the purpose of TypeScript - Not enabling
strict: true— you’re missing half the safety - Confusing
typevsinterface— they’re ~90% interchangeable - Thinking types exist at runtime — they don’t.
typeofat runtime is JavaScript’stypeof, not TypeScript’s - Using
asto silence errors — you’re lying to the compiler - Not using union types — reaching for
enumwhen"a" | "b"suffices - Typing everything explicitly — let inference work; annotate parameters, infer returns
- Confusing
nullvsundefined— usestrictNullChecksand pick one convention - Mutating objects and expecting types to track it — TS tracks control flow, not mutation history
- Not reading error messages carefully — TS errors are verbose but precise
10 Beginner Exercises
- Create a
Usertype with name, email, age (optional). Write a function that greets the user. - Write a function that accepts
string | numberand returns the length (string) or the number doubled. - Create a
TodoItemtype. Write functions to add, remove, toggle completion. - Write a function using a tuple
[string, number]to represent[name, age]. - Create a type for RGB color
{ r: number, g: number, b: number }and a function to convert to hex string. - Write a function that accepts an array of numbers and returns the sum, min, and max as an object.
- Create a discriminated union for
Shape(circle, rectangle, triangle) and a function to calculate area. - Write a
stringifyfunction that handlesstring | number | boolean | null | undefined. - Create an
interfacefor a blog post and extend it for a published post (addspublishedAt: Date). - Write a function that filters an array to only include items matching a predicate, preserving the type.
Level 2 — Junior
Generics
// The most important concept for reusable type-safe code
// Generic function
function identity<T>(value: T): T {
return value;
}
const str = identity('hello'); // T inferred as "hello" (literal)
const num = identity(42); // T inferred as 42
// Generic constraint
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
// WHY constraints matter: Without `extends`, you can't access `.length`
// because T could be anything. Constraints narrow the type parameter.
// Generic interface
interface Repository<T> {
findById(id: string): T | null;
findAll(): T[];
save(entity: T): void;
}
Utility Types
// Built-in mapped types — KNOW THESE COLD
type User = { name: string; age: number; email: string };
type PartialUser = Partial<User>; // all optional
type RequiredUser = Required<PartialUser>; // all required
type ReadonlyUser = Readonly<User>; // all readonly
type NameAndAge = Pick<User, 'name' | 'age'>;
type WithoutEmail = Omit<User, 'email'>;
type StringRecord = Record<string, number>; // { [key: string]: number }
type NonNull = NonNullable<string | null>; // string
type ReturnOfFn = ReturnType<typeof getLength>; // number
type ParamsOfFn = Parameters<typeof identity>; // [value: unknown]
keyof, typeof, Indexed Access
type User = { name: string; age: number; role: 'admin' | 'user' };
type UserKeys = keyof User; // "name" | "age" | "role"
type UserName = User['name']; // string
type UserRole = User['role']; // "admin" | "user"
const config = { api: '/api', timeout: 3000 } as const;
type Config = typeof config; // { readonly api: "/api"; readonly timeout: 3000 }
type ConfigKeys = keyof typeof config; // "api" | "timeout"
// WHY `as const`: Without it, TS widens string/number literals.
// With it, the exact values are preserved as literal types.
Mapped Types
// The foundation of utility types
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; getRole: () => "admin" | "user" }
Discriminated Unions
// THE most important pattern for modeling state in TypeScript
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function handle<T>(result: Result<T>) {
if (result.ok) {
console.log(result.value); // narrowed: { ok: true; value: T }
} else {
console.error(result.error); // narrowed: { ok: false; error: E }
}
}
// WHY: Discriminated unions + exhaustive checks = the safest state modeling.
// The discriminant (`ok`) lets the compiler narrow precisely.
// Add `never` exhaustiveness check:
function assertNever(x: never): never {
throw new Error(`Unexpected: ${x}`);
}
Enums vs Unions
// PREFER string unions over enums in most cases
// Enum — has runtime cost, creates an object
enum Direction {
Up = 'UP',
Down = 'DOWN',
}
// String union — zero runtime cost, just types
type Direction2 = 'UP' | 'DOWN';
// WHY prefer unions:
// 1. No runtime overhead (type erasure)
// 2. Better inference
// 3. Simpler to compose
// 4. No reverse mapping weirdness (numeric enums)
// 5. Works with `as const` objects for iteration
// WHEN enums are OK:
// - You need runtime iteration over values
// - You want computed members
// - You're in a backend-heavy codebase that uses them consistently
// Better alternative: const object + type
const DIRECTIONS = { Up: 'UP', Down: 'DOWN' } as const;
type Direction3 = (typeof DIRECTIONS)[keyof typeof DIRECTIONS]; // "UP" | "DOWN"
Function Overloads
// Use when a function has multiple call signatures with different return types
function parse(input: string): object;
function parse(input: string, reviver: Function): object;
function parse(input: string, reviver?: Function): object {
return reviver ? JSON.parse(input, reviver as any) : JSON.parse(input);
}
// WHY: Overloads give callers precise return types based on their arguments.
// The implementation signature is NOT visible to callers.
// Prefer generics + conditional types over overloads when possible —
// overloads don't compose well.
Common Anti-Patterns
anyas escape hatch → Useunknownand narrow- Type assertions (
as) to bypass errors → Fix the type, don’t lie - Over-using
interface extends→ Composition (intersection&) is more flexible - Giant shared types file → Colocate types with their domain
- Not using
satisfies→satisfieschecks type while preserving inference - Ignoring
readonly→ Mutation bugs are real - Using
objecttype → Too broad. UseRecord<string, unknown>or specific shapes - String-based event systems without literal types → Lose type safety
- Not using discriminated unions for state → Using
isLoading && databooleans - Exporting internal types → Leaky abstractions that break on internal changes
10 Mini Project Ideas
- Type-safe event emitter with literal event names and typed payloads
- Generic
Result<T, E>monad withmap,flatMap,unwrapOr - Type-safe local storage wrapper with schema validation
- Builder pattern for constructing complex objects with type safety
- Type-safe route params extractor from route strings
- Generic form validator with discriminated union error results
- Type-safe state machine (status transitions enforced at type level)
- Typed fetch wrapper with response type inference
- Type-safe configuration loader with defaults and overrides
- Generic repository pattern with typed query builders
Level 3 — Senior
Advanced Generics
// Constraint propagation
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Default type parameters
type ApiResponse<T = unknown, E = { message: string }> =
| { status: 'success'; data: T }
| { status: 'error'; error: E };
// Multiple constraints via intersection
function clone<T extends object & { id: string }>(item: T): T {
return { ...item };
}
Conditional Types
// The type-level `if` statement
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true
// Distributive behavior: naked type params distribute over unions
type D = IsString<string | number>; // true | false → boolean
// Preventing distribution: wrap in tuple
type IsStringStrict<T> = [T] extends [string] ? true : false;
type E = IsStringStrict<string | number>; // false
// WHY distribution exists: It's the intended behavior for type-level
// "map over union." When you write `T extends U`, if T is a union,
// each member is checked individually. This is powerful but surprising.
The infer Keyword
// Extract types from other types — the regex of the type system
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends (infer E)[] ? E : never;
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
// Infer in template literals
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// "userId" | "postId"
Recursive Types
// Types that reference themselves
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// JSON type
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
// Path access type
type Path<T, K extends string> = K extends `${infer Head}.${infer Tail}`
? Head extends keyof T
? Path<T[Head], Tail>
: never
: K extends keyof T
? T[K]
: never;
// WHY recursion limits matter: TS has a depth limit (~50 levels).
// Exceeding it gives "Type instantiation is excessively deep and possibly infinite."
// Design recursive types to terminate quickly.
Template Literal Types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts';
type Route = `${HTTPMethod} ${Endpoint}`;
// "GET /users" | "GET /posts" | "POST /users" | ... (8 combinations)
type CSSProperty = `${string}-${string}`;
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"
Variance — Covariance / Contravariance
// THE concept that separates senior from expert understanding
// Covariance: if Dog extends Animal, then Array<Dog> extends Array<Animal>
// (output/read position — same direction)
// Contravariance: if Dog extends Animal, then
// (handler: Animal => void) extends (handler: Dog => void)
// (input/write position — reversed direction)
// WHY this matters:
type Animal = { name: string };
type Dog = Animal & { breed: string };
// Covariant (safe): readonly arrays, return types, Promises
const dogs: readonly Dog[] = [{ name: 'Rex', breed: 'Lab' }];
const animals: readonly Animal[] = dogs; // ✅ OK
// Contravariant (safe): function parameters in strict mode
type Handler<T> = (item: T) => void;
const animalHandler: Handler<Animal> = (a) => console.log(a.name);
const dogHandler: Handler<Dog> = animalHandler; // ✅ OK — Animal handler can handle Dogs
// TypeScript's `in` and `out` variance annotations (4.7+)
type Producer<out T> = () => T; // covariant
type Consumer<in T> = (item: T) => void; // contravariant
type Processor<in out T> = (item: T) => T; // invariant
Runtime Validation Strategy
// THE CRITICAL INSIGHT: Types don't exist at runtime.
// Every system boundary needs runtime validation.
// Boundaries that NEED runtime validation:
// 1. API responses
// 2. User input (forms)
// 3. URL parameters
// 4. Environment variables
// 5. Database query results
// 6. WebSocket messages
// 7. localStorage/sessionStorage
// 8. PostMessage from iframes/workers
// 9. Third-party SDK responses
// 10. File uploads / parsed files
// Strategy: Schema-first, derive types
// With Zod
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user']),
});
type User = z.infer<typeof UserSchema>; // Type derived from schema
// WHY schema-first:
// 1. Single source of truth (no type/validation drift)
// 2. Runtime safety at boundaries
// 3. Types are always correct (derived, not duplicated)
// 4. Composable schemas = composable types
Monorepo TypeScript Architecture
monorepo/
├── tsconfig.base.json ← shared compiler options
├── packages/
│ ├── shared/
│ │ ├── tsconfig.json ← extends base, uses composite
│ │ ├── src/
│ │ └── package.json
│ ├── ui/
│ │ ├── tsconfig.json ← references shared
│ │ ├── src/
│ │ └── package.json
│ └── app/
│ ├── tsconfig.json ← references shared + ui
│ ├── src/
│ └── package.json
└── package.json
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}
// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
10 Production-Grade Project Examples
- Type-safe API client generator from OpenAPI spec
- Full-stack type-safe app with tRPC + React Query
- Design system with polymorphic components and compound patterns
- Schema-driven form library with runtime validation
- Type-safe i18n system with exhaustive key checking
- Event-driven architecture with typed event bus
- Type-safe CLI tool with command parsing and validation
- Database ORM wrapper with type-safe query builder
- Plugin system with typed extension points
- Monorepo shared package with declaration-only exports
Level 4 — Expert
Type-Level Programming
// Types as a programming language — TypeScript's type system is Turing complete
// Type-level arithmetic (simplified)
type BuildTuple<N extends number, T extends any[] = []> = T['length'] extends N
? T
: BuildTuple<N, [...T, any]>;
type Add<A extends number, B extends number> = [
...BuildTuple<A>,
...BuildTuple<B>,
]['length'];
type Sum = Add<3, 4>; // 7
// String parsing at type level
type Split<
S extends string,
D extends string,
> = S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<'a.b.c', '.'>; // ["a", "b", "c"]
Compiler Mental Model
What the compiler actually does with generics:
function wrap<T>(value: T): { wrapped: T } {
return { wrapped: value };
}
const result = wrap('hello');
// Inference steps:
// 1. Compiler sees call site: wrap("hello")
// 2. "hello" has type "hello" (string literal)
// 3. T is bound to "hello"
// 4. Return type becomes { wrapped: "hello" }
// 5. result: { wrapped: "hello" }
//
// If you wrote wrap<string>("hello"), T = string,
// and you LOSE the literal type. Let inference work.
Inference priority rules:
- Explicit type argument always wins
- Contextual typing (e.g., callback parameter types) is next
- Candidate types from arguments are collected
- Best common type is selected
- If ambiguous, TS widens or falls back to constraints
Distributive Conditional Internals
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Distributes:
// = (string extends any ? string[] : never) | (number extends any ? number[] : never)
// = string[] | number[]
// NOT (string | number)[]
// To get (string | number)[], prevent distribution:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>; // (string | number)[]
Declaration Emit Strategy
// When publishing a library, your .d.ts files ARE your public API
// Rules for clean declaration emit:
// 1. Don't use inferred return types for public functions — annotate them
// 2. Don't export internal types
// 3. Use `/** @internal */` + stripInternal tsconfig option
// 4. Test your declarations with `tsd` or `expect-type`
// 5. Use `composite: true` for project references
// 6. Generate declaration maps for go-to-source in consumers
// Testing declarations:
// npm install -D tsd
import { expectType, expectError } from 'tsd';
expectType<string>(myFunction('input'));
expectError(myFunction(42)); // should error
Architecture Review Checklist
| Area | Question |
|---|---|
| Strictness | Is strict: true enabled? Any any leaks? |
| Boundaries | Are all external inputs validated at runtime? |
| Inference | Are we fighting inference or working with it? |
| Public API | Are exported types intentional and stable? |
| Generics | Are generic constraints tight enough? Too tight? |
| Performance | Are there deeply nested conditional types? |
| Module resolution | Is moduleResolution correct for the runtime? |
| Declaration emit | Do .d.ts files expose internal types? |
| Discriminated unions | Are states modeled as unions, not boolean combos? |
| Error handling | Is Result<T, E> used instead of thrown errors? |
| Schema alignment | Do types and runtime schemas derive from one source? |
| Monorepo | Are project references set up correctly? |
What Expert Engineers Care About That Juniors Miss
- Inference quality — Does the API surface infer well, or do users need to annotate?
- Declaration stability — Changing an internal type shouldn’t break consumer
.d.ts - Type computation cost — Complex types slow down IDE and CI
- Contravariance in callbacks — Function parameter types go in the opposite direction
satisfiesvsas constvs annotation — Each has different inference behavior- Module boundary types — What types cross package boundaries?
- Branded types for nominal safety —
UserIdshould not be assignable toPostId - Excess property checking — Only works on object literals, not variables
- Widening vs narrowing — When and why TS widens or narrows
isolatedModulesimplications — Required for SWC/esbuild, restrictsconst enumand namespace merging
15 Advanced Engineering Discussion Topics
- How do you version public types in a library without breaking consumers?
- When should you use
unknownvs generics for maximum flexibility? - How do you balance type strictness with developer velocity?
- What’s the right granularity for discriminated union variants?
- How do you handle API response types when the backend schema is unreliable?
- Should internal packages in a monorepo export types or just implementations?
- How do you test types (not runtime behavior, but type correctness)?
- When is
anyacceptable in production code? - How do you design generic hooks that compose well?
- What’s the cost of deep
DeepPartial<T>types on compile performance? - How should you type error boundaries in React?
- What’s the right strategy for typing third-party libraries with incomplete types?
- How do you handle type drift between frontend and backend?
- When should you use branded types vs wrapper classes?
- How do you prevent type exports from becoming your library’s implicit API?
Level 5 — Compiler / Language Architect Mindset
TypeScript Compiler Architecture
┌────────────────────────────────────────────────────┐
│ tsc / tsserver │
│ │
│ ┌──────────┐ ┌────────┐ ┌────────┐ ┌─────────┐ │
│ │ Program │──│ Source │──│ Source │──│Compiler │ │
│ │ │ │ File │ │ File │ │ Options │ │
│ └────┬─────┘ └────────┘ └────────┘ └─────────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ Scanner │ → Token stream │
│ └────┬─────┘ │
│ ┌────▼─────┐ │
│ │ Parser │ → AST (SyntaxTree nodes) │
│ └────┬─────┘ │
│ ┌────▼─────┐ │
│ │ Binder │ → Symbols + Scopes │
│ └────┬─────┘ │
│ ┌────▼─────────┐ │
│ │ Type Checker │ → Type resolution, diagnostics │
│ │ (checker.ts) │ ~50K lines, the heart of TS │
│ └────┬─────────┘ │
│ ┌────▼─────┐ │
│ │ Emitter │ → .js / .d.ts / .map │
│ └──────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Language Service │ → IDE features │
│ │ (completions, hover,│ (powers VS Code TS) │
│ │ diagnostics, etc.) │ │
│ └────────────────────┘ │
└────────────────────────────────────────────────────┘
Key files in the compiler:
scanner.ts— tokenizerparser.ts— AST constructionbinder.ts— symbol table creationchecker.ts— type checking (~50K LOC, the brain)emitter.ts— JavaScript outputtransformer.ts— AST transforms before emit
Babel vs tsc vs SWC vs esbuild
| Feature | tsc | Babel | SWC | esbuild |
|---|---|---|---|---|
| Language | TypeScript | JavaScript | Rust | Go |
| Type checking | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Speed | Slow | Medium | Fast | Fastest |
| Emit quality | Reference | Good | Good | Good |
| Plugin system | Transformers | Rich plugins | Limited | Minimal |
| Declaration emit | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Decorators | TS legacy + TC39 | TC39 via plugin | TC39 | Limited |
| Best for | Type checking, .d.ts | Legacy/custom transforms | Fast builds | Bundling |
Key insight: Modern setups use tsc for type checking only (noEmit: true) + SWC/esbuild for transpilation. This gives you the best of both: type safety + speed.
Ergonomics vs Soundness Trade-offs
TypeScript deliberately chose ergonomics over soundness in several cases:
| Unsound Pattern | Why It Exists |
| ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------ |
| Method parameter bivariance | Too many callback patterns would break |
| any type | Escape hatch for gradual adoption |
| Type assertions (as) | Developers know more than the compiler sometimes |
| enum member access | Convenience over safety |
| Index signatures return T not T | undefined | Too noisy (use noUncheckedIndexedAccess) |
| Function arity (fewer params assignable to more) | Callback convention in JS |
Future of TypeScript
- Type annotations proposal (TC39 Stage 1) — types as comments in JS
- Isolated declarations — faster declaration emit without type checking
using/await using— explicit resource management (TS 5.2+)- Decorator metadata — runtime access to decorator info
- Continued focus on performance — faster checking, faster IDE
- Go port of tsc — announced 2025, 10x faster type checking
3. Setup Guide
Installing TypeScript
# Global (for CLI)
npm install -g typescript
# Project-level (recommended)
npm install -D typescript
# Check version
npx tsc --version
tsconfig Setups
Beginner
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
},
"include": ["src"],
}
Production App (React + Next.js)
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"incremental": true,
"resolveJsonModule": true,
"paths": {
"@/*": ["./src/*"],
},
},
"include": ["src", "next-env.d.ts"],
"exclude": ["node_modules"],
}
Library
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"stripInternal": true,
},
"include": ["src"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"],
}
Monorepo Base
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
},
}
Strict Mode Explained
"strict": true enables ALL of these:
| Flag | What It Does | Why It Matters |
|---|---|---|
strictNullChecks | null/undefined not assignable to other types | Prevents the #1 runtime error |
strictFunctionTypes | Contravariant function parameters | Catches callback type bugs |
strictBindCallApply | Typed bind, call, apply | No more any leaking from these |
strictPropertyInitialization | Class properties must be initialized | Catches uninitialized members |
noImplicitAny | Must annotate when inference fails | No hidden any |
noImplicitThis | Must type this in functions | Prevents this confusion |
useUnknownInCatchVariables | catch(e) gives unknown not any | Forces proper error handling |
alwaysStrict | Emits "use strict" | JS strict mode everywhere |
Additional recommended flags beyond strict:
{
"noUncheckedIndexedAccess": true, // obj[key] returns T | undefined
"exactOptionalPropertyTypes": true, // { x?: string } ≠ { x: string | undefined }
"noFallthroughCasesInSwitch": true, // prevents switch fallthrough
"forceConsistentCasingInFileNames": true, // prevents case-sensitivity bugs
}
Path Aliases
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"],
},
},
}
Note: Path aliases in tsconfig are ONLY for the type checker. Your bundler (Vite, webpack, Next.js) must also be configured to resolve them.
ESM vs CJS
// For ESM output (modern):
{
"compilerOptions": {
"module": "ESNext", // or "NodeNext" for Node.js
"moduleResolution": "bundler" // or "nodenext"
}
}
// package.json: "type": "module"
// For CJS output (legacy):
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node"
}
}
// For libraries supporting BOTH (dual package):
// Use tsup or unbuild to build ESM + CJS from ESM source
Recommended Architecture for Your Stack
your-project/
├── tsconfig.base.json ← strict, shared options
├── apps/
│ ├── web/ ← Next.js app
│ │ ├── tsconfig.json ← extends base
│ │ └── src/
│ └── docs/ ← Astro site
│ ├── tsconfig.json
│ └── src/
├── packages/
│ ├── ui/ ← Shared React components
│ │ ├── tsconfig.json ← composite: true
│ │ └── src/
│ ├── shared/ ← Shared types + utils
│ │ ├── tsconfig.json ← composite: true
│ │ └── src/
│ └── config/ ← Shared configs (TS, ESLint)
│ └── tsconfig/
├── turbo.json ← Turborepo config
└── package.json
4. TypeScript Tooling Comparison
Build & Transpilation Tools
| Tool | Philosophy | Type Check | Speed | DX | Library Author | When To Use | When NOT |
|---|---|---|---|---|---|---|---|
| tsc | Reference compiler | ✅ | Slow | Good | ✅ .d.ts emit | Type checking, declarations | Fast builds |
| Babel | JS transforms | ❌ | Medium | Rich plugins | ⚠️ | Legacy, custom transforms | New projects |
| SWC | Fast Rust compiler | ❌ | Very fast | Good | ⚠️ | Next.js, fast builds | Need type checking |
| esbuild | Ultra-fast bundler | ❌ | Fastest | Minimal config | ⚠️ | Bundling, dev servers | Complex transforms |
| tsup | Zero-config lib build | ❌ (uses esbuild) | Fast | Excellent | ✅ | Library publishing | App builds |
| tsx | TS execution | ❌ | Fast | Instant run | ❌ | Scripts, dev | Production |
| ts-node | TS execution | Optional | Slow | OK | ❌ | Legacy scripts | New projects |
| Vite | Dev-first bundler | ❌ (uses esbuild) | Fast | Best-in-class | ⚠️ | App dev servers | Libraries |
Linting & Formatting
| Tool | Philosophy | Speed | TS Support | When To Use |
|---|---|---|---|---|
| ESLint | Extensible linting | Slow | Via typescript-eslint | Need custom rules, TS-aware linting |
| Biome | Fast all-in-one | Very fast | Native | Speed-focused, simpler rulesets |
Runtime Validation
| Tool | Philosophy | Bundle Size | Composability | Inference | When To Use |
|---|---|---|---|---|---|
| Zod | Schema-first | ~14KB | Excellent | ✅ z.infer | Most projects, best ecosystem |
| Valibot | Modular/tree-shakeable | ~1KB used | Excellent | ✅ | Bundle-sensitive projects |
| io-ts | FP-style codecs | ~5KB | FP-heavy | ✅ | FP-oriented codebases |
Recommendation for your stack: Zod for most things. Valibot if bundle size matters (Astro).
5. Cheatsheet
Type Syntax Quick Reference
// Primitives
string, number, boolean, null, undefined, bigint, symbol, void, never, unknown, any
// Object
{ key: Type }
{ key?: Type } // optional
{ readonly key: Type } // immutable
// Array
Type[]
Array<Type>
[Type, Type] // tuple
readonly Type[] // immutable array
// Function
(a: A, b: B) => Return
(...args: A[]) => Return
// Union & Intersection
A | B // either
A & B // both
// Literal
"exact" | 42 | true
// Template Literal
`prefix${string}suffix`
Generic Patterns
// Identity
<T>(x: T) => T
// Constrained
<T extends string>(x: T) => T
// Default
<T = string>(x: T) => T
// Multiple
<T, U>(x: T, y: U) => [T, U]
// Constrained by another param
<T, K extends keyof T>(obj: T, key: K) => T[K]
Conditional Type Patterns
T extends U ? X : Y // basic
T extends infer U ? U : never // extract
T extends (...args: infer P) => infer R ? [P, R] : never // function parts
[T] extends [U] ? X : Y // non-distributive
T extends any ? T[] : never // distributive map
Utility Type Implementations
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;
type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: never;
type MyParameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
Narrowing Patterns
typeof x === 'string'; // typeof guard
x instanceof MyClass; // instanceof guard
'key' in obj; // in operator
x === null; // equality
x != null; // non-null
Array.isArray(x); // array check
x.kind === 'circle'; // discriminant
// Custom type guard
function isString(x: unknown): x is string {
return typeof x === 'string';
}
// Assertion function
function assertDefined<T>(x: T | undefined): asserts x is T {
if (x === undefined) throw new Error('Expected defined');
}
React Typing Patterns
// Component props
type Props = {
children: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
variant?: "primary" | "secondary";
};
// FC (optional — you can just type the function directly)
const Button = ({ children, onClick, variant = "primary" }: Props) => { ... };
// forwardRef
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => { ... });
// Polymorphic component
type PolymorphicProps<C extends React.ElementType> = {
as?: C;
} & React.ComponentPropsWithoutRef<C>;
// Generic component
function List<T>({ items, renderItem }: {
items: T[];
renderItem: (item: T) => React.ReactNode;
}) { ... }
// Event handlers
onChange: React.ChangeEventHandler<HTMLInputElement>
onSubmit: React.FormEventHandler<HTMLFormElement>
// Context with non-null
const MyContext = React.createContext<ContextType | null>(null);
function useMyContext() {
const ctx = React.useContext(MyContext);
if (!ctx) throw new Error("Missing provider");
return ctx;
}
Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
Type 'X' is not assignable to type 'Y' | Shape mismatch | Check property types |
Object is possibly 'undefined' | Missing null check | Add if (x) or x! or x ?? default |
Type 'string' is not assignable to type '"a" | "b"' | Widened literal | Use as const or explicit annotation |
Property does not exist on type | Missing property in type | Add to type or narrow first |
Type instantiation is excessively deep | Recursive type too deep | Simplify recursion, add termination |
Argument not assignable to parameter | Wrong argument type | Check function signature |
Cannot find module | Module resolution failure | Check paths, moduleResolution, types |
'X' refers to a value, but is used as a type | Mixing value/type namespaces | Use typeof X or import the type |
Performance Tips
- Use
interfaceovertypefor object shapes (cached by name) - Avoid deep recursive types — cap at 3-4 levels
- Use
skipLibCheck: true— don’t re-checknode_modulestypes - Use
incremental: true— caches previous results - Avoid complex conditional types in hot paths
- Prefer
satisfiesoveras const+ assertion chains - Use project references in monorepos for parallel checking
- Profile with
--generateTraceflag
6. Real-World Engineering Mindset
API Typing
Problem: API responses come from outside the type system. Types can lie.
Strategies:
| Strategy | Approach | Pros | Cons | Best For |
|---|---|---|---|---|
| Trust the backend | Cast response.json() as User | Simple | Unsafe, types can drift | Prototypes |
| Runtime validation | UserSchema.parse(data) | Safe, single source of truth | Bundle size, perf | Production |
| Code generation | OpenAPI → types + client | Auto-synced, accurate | Build step, rigidity | Large APIs |
| tRPC | End-to-end type inference | Zero drift, amazing DX | Requires Node backend | Full-stack TS |
Senior choice: tRPC if you own the backend. OpenAPI codegen + Zod validation otherwise.
React Component Typing
Problem: Components need to be flexible, composable, and type-safe.
// ❌ Don't: Overly permissive
type Props = { [key: string]: any };
// ❌ Don't: Overly restrictive
type Props = { variant: 'primary'; color: 'blue' };
// ✅ Do: Constrained flexibility
type Props = {
variant: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
} & Omit<React.ComponentPropsWithRef<'button'>, 'className'>;
// ✅ Do: Polymorphic for design systems
type ButtonProps<C extends React.ElementType = 'button'> = {
as?: C;
variant: 'primary' | 'secondary';
} & Omit<React.ComponentPropsWithoutRef<C>, 'as' | 'variant'>;
Generic Hooks
// Pattern: Generic hook with constrained inference
function useLocalStorage<T>(
key: string,
initialValue: T,
schema: z.ZodType<T>, // runtime validation!
): [T, (value: T | ((prev: T) => T)) => void] {
const [stored, setStored] = useState<T>(() => {
const item = localStorage.getItem(key);
if (!item) return initialValue;
const parsed = schema.safeParse(JSON.parse(item));
return parsed.success ? parsed.data : initialValue;
});
// ...
}
// WHY schema parameter: localStorage is a system boundary.
// Types don't exist at runtime. Schema validates at the boundary.
SDK Design
// Pattern: Builder-style type-safe SDK
class APIClient<TRoutes extends Record<string, RouteConfig>> {
constructor(private routes: TRoutes) {}
call<K extends keyof TRoutes>(
route: K,
input: TRoutes[K]['input'],
): Promise<TRoutes[K]['output']> {
// ...
}
}
// Type-safe config
const client = new APIClient({
getUser: {
method: 'GET',
path: '/users/:id',
input: z.object({ id: z.string() }),
output: z.object({ name: z.string() }),
},
} as const);
// Usage: fully inferred
const user = await client.call('getUser', { id: '123' });
// user: { name: string }
Schema-Driven Development
// THE strategy for modern TypeScript apps:
// Schema is the source of truth for BOTH types and validation
// 1. Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user']),
metadata: z.record(z.unknown()).optional(),
});
// 2. Derive type
type User = z.infer<typeof UserSchema>;
// 3. Use everywhere
// - API response validation
// - Form validation
// - Database row typing
// - Event payload typing
// WHY: One change to the schema updates BOTH the type and the validation.
// No drift. No duplication. No "I updated the type but forgot the validator."
Monorepo Shared Types
Problem: Types shared across packages can create tight coupling.
Strategy:
// packages/shared/src/types.ts — shared domain types
export type UserId = string & { __brand: "UserId" };
export type PostId = string & { __brand: "PostId" };
export type User = {
id: UserId;
name: string;
};
// Schemas live here too
export const UserSchema = z.object({ ... });
export type User = z.infer<typeof UserSchema>;
// RULE: Shared types should be DOMAIN types, not implementation types.
// Don't share React component prop types across packages.
// Don't share internal state types.
// Share: entities, value objects, API contracts, events.
7. Brainstorm / Open Questions
Type Systems (10)
- Should this return type be inferred or explicitly annotated? When does explicit annotation improve clarity vs harm DX?
- Is this generic truly necessary, or is it adding complexity for a single use case?
- What happens when your conditional type distributes unexpectedly over a union?
- Can you model this domain concept with discriminated unions instead of class hierarchies?
- When is
unknownthe right choice over a genericT? - How do you handle types that are “almost the same” across contexts (e.g., API vs DB vs UI)?
- Should you use branded types for IDs, or is
stringgood enough? - How do you type a function that returns different types based on runtime configuration?
- What’s the type-level equivalent of “dependency injection”? (Answer: generics + constraints)
- When does structural typing cause bugs that nominal typing would prevent?
API Design (10)
- Should API client types be generated or hand-written?
- How do you handle API versioning in your type layer?
- What happens to consumers when you change a shared type?
- Should you use Zod schemas at the API boundary, at the consumer, or both?
- How do you type a REST API client that works with any endpoint shape?
- When is tRPC overkill? When is it essential?
- Should GraphQL types be generated from the schema or hand-maintained?
- How do you type middleware that transforms request/response shapes?
- What’s the right abstraction level for a typed HTTP client?
- How do you handle partial responses and sparse fieldsets in types?
Runtime vs Compile-Time (10)
- Where are the boundaries in your system where types stop being trustworthy?
- Are you validating at every system boundary, or only some?
- What runtime guarantees does TypeScript NOT provide that you assume it does?
- How do you handle
JSON.parse()— the most dangerous untyped operation? - Should you use
satisfiesoras constto preserve literals at the boundary? - How do you type
localStorage/sessionStoragesafely? - What happens when a third-party API changes shape but your types don’t?
- Should environment variables be typed with
z.env()or manual casting? - How do you type
postMessagebetween workers/iframes? - When should runtime validation throw vs return
Result<T, E>?
DX (10)
- Does this type improve autocomplete, or does it make it worse?
- Is this error message helpful to the developer, or cryptic?
- Would
satisfiesgive better inference than explicit annotation here? - Are your generic constraints helping inference or fighting it?
- Does this type compose well, or does it force users to unwrap/re-wrap?
- Would a simpler type with a runtime check give better DX?
- Is this overloaded function signature really needed, or would a union input suffice?
- How many levels deep can a developer read this type before giving up?
- Does your library require users to write
as constto get good inference? - Would a builder pattern give better autocomplete than a config object?
Compiler Behavior (10)
- Why does TypeScript infer
stringhere instead of the literal type? - Why does this generic fail to infer when there are multiple inference sites?
- What’s the difference between
T extends Xin a conditional vs in a constraint? - Why does excess property checking only work on object literals?
- How does control flow analysis interact with closures and callbacks?
- Why does
readonlynot work as deeply as you’d expect? - When does TypeScript widen a type, and can you prevent it?
- Why does
satisfiespreserve the narrower type while:annotation doesn’t? - What happens to
neverin a union vs an intersection? - How does the compiler decide which overload to select?
Performance (10)
- How does this conditional type affect
tsc --watchperformance? - Is this recursive type hitting the depth limit in practice?
- Would
interfacebe faster thantypehere for the compiler? - Are you using
skipLibCheckto avoid re-checkingnode_modules? - Would project references speed up your monorepo builds?
- How do you profile TypeScript compilation performance?
- Is this mapped type being instantiated too many times?
- Should you cache complex computed types with intermediate type aliases?
- How does
--generateTracehelp identify type-checking bottlenecks? - What’s the performance impact of
noUncheckedIndexedAccessin a large codebase?
Library Architecture (10)
- What types should be in your public API surface vs internal?
- How do you version breaking type changes independently from runtime changes?
- Should your library export Zod schemas, types, or both?
- How do you test that your declaration files are correct?
- What’s the right amount of generics for a library API?
- Should your library use
interfaceortypefor extensibility? - How do you handle peer dependency types (e.g., React types)?
- When should a library use
consttype parameters (TS 5.0)? - How do you design types that survive a major version upgrade?
- Should internal package types be re-exported from a barrel file?
Large-Scale Systems (12)
- What type architecture scales across 50+ packages?
- How do you prevent type changes in one package from cascading failures?
- Should you use a shared types package or derive types locally?
- How do you handle type alignment between frontend and backend in a monorepo?
- What’s the migration strategy for adding strict mode to a 200K LOC codebase?
- How do you enforce type conventions across teams?
- Should you use module augmentation for third-party types or wrapper types?
- What’s the right level of type abstraction for a platform team?
- How do you handle gradual TypeScript adoption in a legacy JS codebase?
- When should you write custom ESLint rules for type conventions?
- How do you maintain type safety across micro-frontends?
- What’s the strategy for typing a shared design system used by 10 teams?
8. Practice Questions
Beginner (25 Questions)
Q1
Type: What is the resulting type?
const x = 'hello';
Answer: "hello" (string literal type)
Why: const declarations infer the narrowest possible type. Since x can never be reassigned, TS infers the literal "hello" instead of string.
Q2
Type: What is the resulting type?
let x = 'hello';
Answer: string
Why: let can be reassigned, so TS widens to string.
Q3
Type: True or False: TypeScript types exist at runtime. Answer: False Why: All types are erased during compilation. At runtime, only JavaScript exists.
Q4
Type: What error does this produce?
function greet(name: string): string {
return 42;
}
Answer: Type 'number' is not assignable to type 'string'
Why: The return type is annotated as string but the function returns a number.
Q5
Type: What is the resulting type?
const arr = [1, 'two', true];
Answer: (string | number | boolean)[]
Why: TS infers the best common type for array elements.
Q6
Type: Fill in the blank: To make a property optional, use ___.
Answer: ? after the property name: { name?: string }
Q7
Type: What is the type of x after the if?
function f(x: string | number) {
if (typeof x === 'string') {
// x is ?
}
}
Answer: string
Why: typeof guard narrows the union.
Q8
Type: Which is correct?
// A
interface User {
name: string;
}
// B
type User = { name: string };
Answer: Both are correct. interface supports declaration merging; type supports unions and intersections. For object shapes, they’re interchangeable.
Q9
Type: What’s wrong?
const user: { name: string; age: number } = { name: 'Alice' };
Answer: Property age is missing. All non-optional properties are required.
Q10
Type: What is the resulting type?
function add(a: number, b: number) {
return a + b;
}
Answer: Return type is inferred as number.
Q11
Type: What does void mean as a return type?
Answer: The function doesn’t return a meaningful value. It can return undefined or nothing. Unlike never, the function DOES complete.
Q12
Type: What’s the difference between null and undefined in TypeScript?
Answer: Both are distinct types when strictNullChecks is on. undefined means “not yet assigned.” null means “intentionally empty.” Convention: pick one and be consistent.
Q13
Type: Debugging. Why does this error?
const obj = { name: 'Alice', age: 30 };
obj.email = 'a@b.com';
Answer: Property email does not exist on { name: string; age: number }. TS inferred the shape at declaration.
Q14
Type: What is a tuple type?
Answer: A fixed-length array with typed positions: [string, number] means exactly 2 elements, first is string, second is number.
Q15
Type: What does unknown mean?
Answer: The type-safe counterpart of any. You can assign anything TO unknown, but you can’t use it without narrowing first. Always prefer unknown over any.
Q16
Type: What is never?
Answer: The type with no values. Represents: unreachable code, impossible types, exhaustiveness checks. A function returning never never completes (throws or infinite loops).
Q17
Type: What’s wrong here?
function f(x: string | number) {
return x.toUpperCase();
}
Answer: toUpperCase doesn’t exist on number. Must narrow first with typeof x === "string".
Q18
Type: True or False: any disables type checking for that value.
Answer: True. any bypasses all type checks and propagates unsafety to everything it touches. Avoid it.
Q19
Type: What is a union type?
Answer: A type that can be one of several types: string | number means “either string OR number.”
Q20
Type: Scenario. A colleague writes as any to fix a type error. What do you suggest?
Answer: Find the root cause. Use unknown + narrowing, fix the type definition, or use a type guard. as any hides bugs.
Q21
Type: What is readonly?
type User = { readonly name: string };
Answer: The property cannot be reassigned after initialization. Compile-time only — not enforced at runtime.
Q22
Type: What does Array.isArray() do to types?
Answer: It’s a built-in type guard. Narrows unknown or a union to the array type.
Q23
Type: What is the result?
type A = string & number;
Answer: never — no value can be both string AND number.
Q24
Type: How do you type a function that accepts any number of arguments?
Answer: Rest parameters: function f(...args: string[]): void
Q25
Type: What is the satisfies operator?
const config = { api: '/api', port: 3000 } satisfies Record<
string,
string | number
>;
Answer: Checks that the value matches the type WITHOUT widening. The inferred type keeps literal types and specific structure, unlike : Type annotation.
Junior (25 Questions)
Q26
Type: What is the resulting type?
function identity<T>(x: T): T {
return x;
}
const result = identity(42);
Answer: 42 (numeric literal type)
Why: Generic T is inferred from the argument. 42 is a literal.
Q27
Type: What does keyof do?
type User = { name: string; age: number };
type Keys = keyof User;
Answer: "name" | "age" — a union of all property name literal types.
Q28
Type: Implement Pick from scratch.
Answer:
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
Q29
Type: What is a discriminated union? Answer: A union where each member has a common literal property (the discriminant) that TypeScript uses for narrowing.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number };
Q30
Type: Why prefer "a" | "b" over enum { A = "a", B = "b" }?
Answer: String unions have zero runtime cost (erased), better inference, simpler composition, and no reverse mapping issues.
Q31
Type: What is typeof in a type position?
const config = { api: '/api' };
type Config = typeof config;
Answer: Extracts the TypeScript type from a JavaScript value. Here: { api: string }.
Q32
Type: What does as const do?
const arr = [1, 2, 3] as const;
Answer: Makes the value deeply readonly with literal types: readonly [1, 2, 3] instead of number[].
Q33
Type: What is the resulting type?
type Partial<T> = { [K in keyof T]?: T[K] };
type Result = Partial<{ a: string; b: number }>;
Answer: { a?: string; b?: number }
Q34
Type: What is an indexed access type?
type User = { name: string; settings: { theme: 'dark' | 'light' } };
type Theme = User['settings']['theme'];
Answer: "dark" | "light" — you can access nested types using bracket notation.
Q35
Type: What is declaration merging?
Answer: Multiple declarations of the same interface name merge into one:
interface Window {
myGlobal: string;
}
// Merges with lib.dom.d.ts Window interface
This is why interface is preferred for extension points. type cannot merge.
Q36
Type: What does Exclude<T, U> do?
Answer: Removes from T any member that is assignable to U:
type Result = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
Q37
Type: What does Extract<T, U> do?
Answer: Keeps from T only members assignable to U:
type Result = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // "a" | "b"
Q38
Type: Write a generic function that safely accesses an object property. Answer:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Q39
Type: What is the difference between Record<string, T> and { [key: string]: T }?
Answer: Functionally identical. Record is the utility type wrapper for index signatures. Record<K, V> is more flexible because K can be a union of literals.
Q40
Type: What’s wrong with this overload?
function f(x: string): string;
function f(x: number): number;
function f(x: string | number) {
return x;
}
Answer: The implementation return type should be string | number. TS uses the implementation signature internally. The overload signatures are what callers see.
Q41
Type: What does NonNullable<T> do?
Answer: Removes null and undefined from T:
type Result = NonNullable<string | null | undefined>; // string
Q42
Type: What is ReturnType<T>?
Answer: Extracts the return type of a function type:
type R = ReturnType<() => string>; // string
Q43
Type: What is a mapped type with key remapping?
Answer: Using as to transform keys:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
Q44
Type: Explain Parameters<T>.
Answer: Extracts function parameter types as a tuple:
type P = Parameters<(a: string, b: number) => void>; // [string, number]
Q45
Type: What is Awaited<T>?
Answer: Recursively unwraps Promise:
type A = Awaited<Promise<Promise<string>>>; // string
Q46
Type: True or False: You can use keyof on a value.
Answer: False. keyof works on types. Use keyof typeof value for values.
Q47
Type: What is satisfies vs as const?
Answer: as const makes values readonly + literal. satisfies checks against a type while preserving inference. They serve different purposes and can be combined:
const x = { a: 1, b: 2 } as const satisfies Record<string, number>;
Q48
Type: What is Omit<T, K>?
Answer: Creates a type with all properties of T except K:
type Without = Omit<{ a: 1; b: 2; c: 3 }, 'a' | 'b'>; // { c: 3 }
Q49
Type: Scenario: You have a large switch on a discriminated union. How do you ensure exhaustiveness?
Answer: Use never in the default case:
default: {
const _exhaustive: never = value;
throw new Error(`Unhandled: ${_exhaustive}`);
}
If you add a new union member and forget a case, TS errors because the new member isn’t assignable to never.
Q50
Type: What are module systems in TypeScript?
Answer: module in tsconfig controls output format. ESNext = ES modules (import/export). CommonJS = Node.js modules (require/exports). NodeNext = Node.js ESM detection based on package.json "type".
Senior (25 Questions)
Q51
Type: What is the resulting type?
type IsString<T> = T extends string ? true : false;
type Result = IsString<string | number>;
Answer: boolean (which is true | false)
Why: Distributive conditional types. T = string | number distributes: IsString<string> | IsString<number> = true | false = boolean.
Q52
Type: How do you prevent distribution? Answer: Wrap both sides in tuples:
type IsString<T> = [T] extends [string] ? true : false;
type Result = IsString<string | number>; // false
Q53
Type: What does infer do in this type?
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
Answer: infer U captures the inner type of Promise. For Promise<string>, U = string. For non-Promise types, returns T unchanged.
Q54
Type: Write a DeepPartial<T> type.
Answer:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
Q55
Type: What is variance and why should you care? Answer: Variance describes how subtyping of container types relates to subtyping of their contents. Covariance (output positions) preserves direction. Contravariance (input positions) reverses it. Matters for: callback typing, generic constraints, array mutability.
Q56
Type: What’s wrong with this code?
const animals: Animal[] = [];
const dogs: Dog[] = [{ name: 'Rex', breed: 'Lab' }];
const animalsRef: Animal[] = dogs; // TS allows this!
animalsRef.push({ name: 'Cat' }); // Pushes non-Dog into dogs array!
Answer: Arrays are covariant in TypeScript (unsound). A Dog[] assigned to Animal[] allows pushing non-Dog animals. Use readonly Dog[] to prevent this.
Q57
Type: Scenario: Your React component needs to accept either onClick or href, but not both.
Answer:
type ButtonProps = { label: string } & (
| { onClick: () => void; href?: never }
| { href: string; onClick?: never }
);
Using never for mutually exclusive props.
Q58
Type: How do you type a function that takes a string path and returns the nested value? Answer:
type Get<T, Path extends string> =
Path extends `${infer Head}.${infer Tail}`
? Head extends keyof T ? Get<T[Head], Tail> : never
: Path extends keyof T ? T[Path] : never;
function get<T, P extends string>(obj: T, path: P): Get<T, P> { ... }
Q59
Type: What is the const type parameter (TS 5.0)?
Answer:
function create<const T>(config: T): T {
return config;
}
const result = create({ a: 1, b: 'two' });
// result: { readonly a: 1; readonly b: "two" }
// Without `const`: { a: number; b: string }
const tells TS to infer literal/readonly types from arguments.
Q60
Type: Why is z.infer<typeof Schema> better than writing the type manually?
Answer: Single source of truth. Manual types can drift from validation logic. With z.infer, the type is always in sync with the schema. Change the schema → type updates automatically.
Q61
Type: Scenario: Your recursive type hits “Type instantiation is excessively deep.” How do you fix it? Answer: 1) Add explicit termination conditions. 2) Reduce depth with iterative approaches (tail-call style). 3) Cache intermediate results with helper types. 4) Simplify the type if possible.
Q62
Type: What is a template literal type used for in practice?
Answer: Route params extraction, event naming (on${Event}), CSS property typing, string pattern validation. Powerful for string-based APIs.
Q63
Type: How do you type a React hook that returns different shapes based on input? Answer: Use overloads or conditional return types:
function useQuery<T>(
key: string,
enabled: true,
): { data: T; isLoading: boolean };
function useQuery<T>(
key: string,
enabled: false,
): { data: undefined; isLoading: false };
Q64
Type: What is asserts x is T?
Answer: An assertion function signature. After calling, the compiler narrows the type:
function assertString(x: unknown): asserts x is string {
if (typeof x !== 'string') throw new Error('Not string');
}
assertString(value);
// value is now string
Q65
Type: What’s the difference between type A = B & C and interface A extends B, C {}?
Answer: Intersection (&) works with any types, resolves conflicts as never. Interface extends only works with object types, errors on conflicts. Interface extends is slightly faster for the compiler.
Q66
Type: How should you type errors in TypeScript?
Answer: Use discriminated union Result<T, E> instead of throwing:
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
TypeScript can’t track thrown exceptions in types. Result makes errors explicit and type-safe.
Q67
Type: What is isolatedModules and why enable it?
Answer: Ensures each file can be transpiled independently (no cross-file type analysis). Required for SWC, esbuild, Babel. Prohibits const enum, namespace merging across files, re-exporting types without type keyword.
Q68
Type: What is verbatimModuleSyntax (TS 5.0)?
Answer: Replaces isolatedModules + importsNotUsedAsValues. Requires import type for type-only imports. Makes import/export behavior match the module system exactly. Recommended for all new projects.
Q69
Type: Write a type that extracts all keys of T whose values are of type V.
Answer:
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
type StringKeys = KeysOfType<{ a: string; b: number; c: string }, string>;
// "a" | "c"
Q70
Type: What is a branded type? Answer: Simulated nominal typing using intersection with a phantom property:
type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };
function getUser(id: UserId) { ... }
getUser("123" as UserId); // ✅
getUser("123" as PostId); // ❌ Type error
Q71
Type: Scenario: You’re building a component library. Should you export your types from a barrel file?
Answer: Export only public API types from the barrel. Use /** @internal */ + stripInternal for implementation types. Test your public API surface with tsd. A barrel re-export makes every exported type part of your contract.
Q72
Type: How do you type React.forwardRef with generics?
Answer: You can’t directly. Workaround:
function GenericList<T>(props: ListProps<T>, ref: React.Ref<HTMLDivElement>) { ... }
export default React.forwardRef(GenericList) as <T>(
props: ListProps<T> & React.RefAttributes<HTMLDivElement>
) => React.ReactElement;
Q73
Type: What’s the difference between composite: true and references in tsconfig?
Answer: composite makes a project buildable as a dependency (requires declaration, produces .tsbuildinfo). references declares dependencies on other composite projects. Together they enable incremental multi-project builds.
Q74
Type: What is the NoInfer<T> utility type (TS 5.4)?
Answer: Prevents a type parameter from being used as an inference site:
function foo<T>(a: T, b: NoInfer<T>) { ... }
foo("hello", 42); // Error: 42 not assignable to string
// Without NoInfer, T would be inferred as string | number
Q75
Type: What is covariance in React component props? Answer: Props are in input position (consumed by the component), so they should be contravariant. But TS treats method signatures as bivariant for compatibility. This can cause bugs when passing wider prop types to narrower expectations.
Expert / Compiler-Level (25 Questions)
Q76
Type: Why does TypeScript use structural typing instead of nominal typing?
Answer: JavaScript has no type declarations at runtime. Structural typing matches JS’s duck-typing nature. Nominal typing would require inheritance or explicit implements, which doesn’t match how JS developers think.
Q77
Type: What happens internally when the compiler processes T extends U ? X : Y where T is a union?
Answer: The compiler iterates over each union member, checks extends individually, collects results, and unions them. This is the “distribution” behavior. It happens ONLY when T is a naked type parameter (not wrapped in a tuple/array/etc).
Q78
Type: Your generic utility causes compile times to explode. What should you investigate?
Answer: 1) Deeply nested conditional types. 2) Unbounded recursion. 3) Large union type distribution (N members → N checks). 4) Mapped types over large objects. 5) Use --generateTrace to profile. 6) Check if intermediate types can be cached via type aliases.
Q79
Type: What is the difference between checker.ts and binder.ts in the compiler?
Answer: Binder creates symbols and sets up scopes (name resolution). Checker resolves types, checks assignability, produces diagnostics. Binder runs once per file. Checker can be lazy (types resolved on demand).
Q80
Type: Why can’t TypeScript narrow through function calls?
function isString(x: unknown): boolean {
return typeof x === 'string';
}
let val: unknown = 'hello';
if (isString(val)) {
val.toUpperCase(); // Error! Still unknown
}
Answer: TS doesn’t track narrowing across function boundaries (control flow doesn’t cross calls). Solution: use a type predicate x is string as return type.
Q81
Type: What is the in and out variance annotation (TS 4.7)?
Answer:
type Producer<out T> = { get(): T }; // covariant
type Consumer<in T> = { set(v: T): void }; // contravariant
These are explicit declarations of variance. Without them, TS infers variance structurally (slower). With them, the compiler can short-circuit variance checks (faster, more correct).
Q82
Type: What is “declaration emit” and why does it matter?
Answer: Generating .d.ts files from .ts source. Critical for libraries. Challenges: inferred types may reference private types, complex expressions may produce huge declarations, internal types may leak. Use stripInternal, explicit return types on public functions, and test with tsd.
Q83
Type: True or False: const enum values are inlined at every usage site.
Answer: True — tsc replaces references with literal values. But: isolatedModules mode disables this (SWC/esbuild can’t inline across files). Prefer regular unions over const enum.
Q84
Type: What does --generateTrace do?
Answer: Produces Chrome trace files showing where the compiler spends time. Open in chrome://tracing. Shows: type checking duration per file, expensive type instantiations, slow conditional types. Essential for performance debugging.
Q85
Type: Why does this fail?
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type Result = Foo<{ a: string; b: number }>;
Answer: U is inferred from both positions. Since a: string and b: number, the compiler must find a type that satisfies both. In covariant position, it unions: string | number. In contravariant position, it intersects: string & number = never. Here both are covariant (property values), so Result = string | number.
Q86
Type: What is module resolution bundler vs nodenext?
Answer: bundler assumes a bundler (Vite, webpack) resolves imports — no file extensions required, supports exports field. nodenext follows Node.js ESM rules strictly — requires .js extensions, respects "type": "module". Use bundler for apps, nodenext for Node.js libraries.
Q87
Type: What is “isolated declarations” (TS 5.5)?
Answer: A mode where .d.ts files can be generated without running the full type checker — only local syntax information is used. Requires explicit return type annotations on exported functions. Enables parallel declaration emit by tools like SWC/esbuild.
Q88
Type: Why does never disappear from unions but dominate intersections?
Answer: never is the bottom type (empty set). Union: T | never = T (adding nothing). Intersection: T & never = never (intersecting with nothing). Same as set theory: A ∪ ∅ = A, A ∩ ∅ = ∅.
Q89
Type: How does the TypeScript language service differ from tsc?
Answer: tsc is a batch compiler (input → output). The language service is an incremental, interactive API used by IDEs for completions, hover info, diagnostics, refactoring. It maintains a program state and updates incrementally. tsserver wraps the language service in a process with a JSON protocol.
Q90
Type: What is the difference between extends in generics vs conditional types?
Answer: In generics (T extends U): constraint — limits what T can be. In conditional types (T extends U ? X : Y): test — checks if T is assignable to U and branches.
Q91
Type: What is a custom transformer?
Answer: A function that manipulates the AST between parsing and emit. Used for: compile-time code generation, macro-like behavior, custom syntax transforms. Registered via ts.CustomTransformers. Example: typescript-plugin-styled-components.
Q92
Type: What happens when you use Omit with a union type?
type A = { kind: 'a'; x: number } | { kind: 'b'; y: string };
type Result = Omit<A, 'kind'>;
Answer: Omit doesn’t distribute over unions. keyof A is only the common keys ("kind"). After omitting "kind", you get {} — losing all information. To handle union Omit, use a distributive version:
type DistributiveOmit<T, K extends keyof any> = T extends any
? Omit<T, K>
: never;
Q93
Type: How does satisfies work internally?
Answer: It checks that the expression is assignable to the given type but returns the expression’s inferred type (not the target type). Unlike annotation (: Type), which widens to the annotation type. This gives you type checking AND narrow inference.
Q94
Type: What is the “homomorphic mapped type” optimization?
Answer: When a mapped type maps over keyof T (where T is a type parameter), TS preserves optional/readonly modifiers from the original. This is why Partial<T> adds ? but doesn’t lose readonly. Non-homomorphic mapped types (mapping over arbitrary keys) don’t preserve modifiers.
Q95
Type: Design question: How would you type a plugin system where plugins can extend the host’s type? Answer: Use module augmentation + interface merging:
// host
interface PluginRegistry {}
function getPlugin<K extends keyof PluginRegistry>(name: K): PluginRegistry[K];
// plugin
declare module 'host' {
interface PluginRegistry {
myPlugin: { doStuff(): void };
}
}
Q96
Type: Why might ReturnType fail on overloaded functions?
Answer: ReturnType uses the LAST overload signature. If a function has 3 overloads, only the last one’s return type is extracted. This is a known limitation. Workaround: use conditional types matching specific parameter patterns.
Q97
Type: What is bivariance and where does TS use it?
Answer: Bivariance means a type position accepts both wider AND narrower types. TS uses bivariance for method parameters (method(x: T) syntax) for compatibility. strictFunctionTypes only makes arrow/function-expression parameters contravariant, not method shorthand.
Q98
Type: How does noUncheckedIndexedAccess change typing behavior?
Answer: Without it: obj[key] returns T. With it: returns T | undefined. Forces null checking on every dynamic access. Trade-off: safer but more verbose. Recommended for new projects.
Q99
Type: What is the Go port of tsc and why was it built? Answer: Announced in 2025. Rewrites the TypeScript compiler in Go for ~10x faster type checking. Motivation: tsc is single-threaded JS, and type checking is the bottleneck in large codebases. The Go port can parallelize and use more efficient data structures.
Q100
Type: Architecture question: You’re designing a monorepo with 50 packages. What type architecture prevents cascading type breakage? Answer:
- Define domain types in a shared package with explicit versioning
- Use interface boundaries between packages — never export implementation types
- Use project references for incremental builds
- Use
stripInternalto hide internal types from.d.ts - Test public API types with
tsd - Use branded types for cross-package IDs
- Version shared type packages independently
- Use schema-first (Zod) for any types crossing runtime boundaries
9. Personalized Recommendations
For Your Stack: React + Next.js + Astro + Monorepos
Most Important Concepts (Prioritized)
- Discriminated unions — for state modeling (loading/success/error)
- Generics — for reusable hooks, components, utilities
- Runtime validation (Zod) — for API boundaries, form data, env vars
satisfies— for config objects, route definitions- Template literal types — for route typing, event naming
- Branded types — for IDs across domains
as const— for config objects, route arrays- Conditional types +
infer— for library/utility authoring
Advanced Topics to Prioritize
- Polymorphic component typing (design systems)
- Generic React hooks with proper inference
- Schema-driven development (Zod everywhere)
- tRPC or OpenAPI codegen for API safety
- Monorepo type architecture with project references
- Declaration file authoring (if building internal packages)
- Variance understanding (callback typing)
- Compile performance optimization
Common Frontend Engineer Mistakes
- Not validating API responses — “TypeScript catches it” (no, it doesn’t at runtime)
- Over-typing React props — typing every possible HTML attribute manually instead of using
ComponentPropsWithRef - Using
anyfor event handlers — useReact.ChangeEvent<HTMLInputElement>etc. - Not using discriminated unions for state — using
isLoading && databoolean soup - Ignoring
as const— losing literal types for route definitions, action types - Fighting inference — annotating everything instead of letting generics infer
- Not using
satisfies— losing narrow types in config objects - Sharing implementation types across packages — leaky abstractions
60-Day Learning Plan
Days 1–10: Foundations Audit
- Review strict mode flags — ensure all are enabled
- Convert 3 existing components to use discriminated unions
- Add Zod validation to all API calls
- Practice: Q1–Q25
Days 11–20: Generics Mastery
- Build 3 generic React hooks
- Implement 5 utility types from scratch
- Understand
keyof,typeof, indexed access - Practice: Q26–Q50
Days 21–35: Advanced Patterns
- Implement a polymorphic
<Box as={...}>component - Build a type-safe event emitter
- Create a type-safe router with param extraction
- Explore
infer, conditional types, recursive types - Practice: Q51–Q75
Days 36–45: Architecture
- Set up monorepo with project references
- Create a shared types package with branded IDs
- Implement schema-driven form validation
- Design a type-safe API client layer
Days 46–55: Library Authoring
- Build a small utility library with proper
.d.tsoutput - Test declarations with
tsd - Profile compile performance with
--generateTrace - Practice: Q76–Q100
Days 56–60: Review & Strategy
- Review all brainstorm questions (Section 7)
- Write an ADR for your team’s TypeScript conventions
- Create a TypeScript architecture review checklist
- Plan next learning targets from Advanced Topics (Section 11)
10. Official Documentation & Reference Links
Beginner
Intermediate
- TypeScript Handbook — Generics
- TypeScript Handbook — Utility Types
- TypeScript Handbook — Narrowing
- TypeScript Handbook — Mapped Types
- TypeScript Handbook — Conditional Types
- React TypeScript Cheatsheet
- Total TypeScript
- Effective TypeScript
- type-fest — Collection of essential TypeScript types
- ts-pattern — Exhaustive pattern matching
Advanced
- TypeScript Handbook — Template Literal Types
- TypeScript Handbook — Variance
- TypeScript — Project References
- TypeScript — Module Resolution
- Zod Documentation
- tRPC Documentation
- TanStack Query
- TypeScript Performance Wiki
- tsd — Test TypeScript Declarations
- tsup — Bundle TypeScript libraries
- arethetypeswrong — Check package type correctness
Expert / Compiler Internals
- TypeScript Compiler Source
- TypeScript Compiler Notes
- TypeScript Architecture Overview
- TypeScript — checker.ts
- TypeScript Design Goals
- TypeScript Performance Tracing
- TypeScript Compiler API
- TypeScript Language Service API
- Basarat’s TypeScript Deep Dive
11. Advanced Engineering Topics
Type-Level Programming
TypeScript’s type system is Turing complete. You can compute at the type level:
// Type-level string manipulation
type CamelCase<S extends string> = S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S;
type Result = CamelCase<'hello_world_foo'>; // "helloWorldFoo"
// Type-level JSON parser (extreme example — don't do this in production)
// Demonstrates that TS types can parse strings, but at severe performance cost
When to use type-level programming: Library APIs, route typing, config validation. When NOT to: Application code. If it’s not in a library consumed by many, keep it simple.
Compile-Time Performance Optimization
- Use
interfaceovertypefor object shapes — interfaces are cached - Avoid deep conditional type chains — each level multiplies computation
- Use
--incremental— caches type information across builds - Use project references — enables parallel checking
- Profile with
--generateTrace— find the actual bottleneck - Limit union size — 100+ member unions slow down assignability checks
- Avoid
Omiton unions — use distributive version - Use explicit type annotations on public APIs — reduces inference work
- Enable
skipLibCheck— don’t re-checknode_modules
Soundness Trade-offs
| Sound | TypeScript’s Choice | Why |
|---|---|---|
| Method bivariance is unsound | Allow it | Too many callback patterns break |
| Array covariance is unsound | Allow it | Disallowing breaks common patterns |
Index access returns T not T | undefined | Allow (opt-in fix via flag) | Too noisy for most code |
any exists | Allow it | Gradual adoption |
| Type assertions bypass checks | Allow it | Developer autonomy |
Philosophy: TypeScript optimizes for practical JavaScript usage over mathematical type theory. The goal is catching 95% of bugs with 5% of the effort, not proving program correctness.
Future of TypeScript
- Go port — 10x faster type checking, same semantics
- Type annotations in JS (TC39) — types as comments, no build step
- Isolated declarations — parallel
.d.tsemit without type checking - Improved performance — ongoing work on checker speed
- Better ESM support — continued refinement of module resolution
- Decorator metadata — runtime type information (limited, opt-in)
Summary
Key Takeaways
- TypeScript is a compile-time type system that is completely erased at runtime. Never trust types at system boundaries.
- Structural typing is the core philosophy. Shapes matter, names don’t.
- Inference is your best friend. Annotate inputs, let outputs be inferred.
- Discriminated unions are THE pattern for modeling state safely.
- Schema-first development (Zod) bridges the runtime/compile-time gap.
- Generics + constraints are the foundation of reusable type-safe code.
- Variance matters for callbacks, arrays, and generic containers.
- Performance is a real concern — profile before optimizing types.
- Library authors have different concerns than app developers — declaration emit, public API stability, inference quality.
- The compiler is your pair programmer. Understand its mental model and it will catch your mistakes before users do.
Next Steps
- Enable ALL strict flags in your projects today
- Add Zod to every API boundary
- Convert 5 boolean-state components to discriminated unions
- Build one generic utility type from scratch
- Read the TypeScript Performance wiki
- Follow the 60-day plan in Section 9
Advanced Topics to Continue Learning
- Custom TypeScript compiler plugins
- Type-safe distributed event systems
- Compiler API for code generation
- Building TypeScript ESLint rules
- Contributing to DefinitelyTyped
- Reading the TypeScript compiler source (
checker.ts) - Exploring the Go port when released