📚 Table of Contents
- Quick Type System Refresher
- How Type Narrowing Works Internally
- Conditional Types — Beyond Basics
- Distributive Conditional Types Deep Mechanics
- The
inferKeyword — Pattern Matching in Types - Mapped Types Internals
- Key Remapping & Property Filtering
- Template Literal Types as a Type-Level DSL
- Recursive Types & Type-Level Recursion
- Building Advanced Custom Utility Types
- Reverse Engineering DefinitelyTyped
- Variance — The Hidden Foundation
- Union to Intersection — The “Variance Hack” Explained
- Higher-Order Types & Type-Level Function Composition
- Advanced
inferTricks (Tuples, Promises, and More) - Type-Level Parsing (String Manipulation)
- Building a Fully Type-Safe Event System
- Exhaustiveness Checking with
never - Compiler Limits — What Actually Breaks
- Performance Impact of Complex Types
- Practical Guidelines for Production Systems
1️⃣ Quick Type System Refresher (In 5 Minutes)
TypeScript is:
- Structurally typed
- Gradually typed
- Erased at runtime
- Control-flow aware
- Turing complete at the type level (with limits)
Structural Typing
type User = { id: string; name: string };
type Admin = { id: string; name: string; role: string };
const admin: Admin = {
id: "1",
name: "Ahmed",
role: "super",
};
const user: User = admin; // ✅ Valid (structural typing)
`
Type compatibility depends on structure, not name.
2️⃣ How Type Narrowing Works Internally
Type narrowing is powered by Control Flow Analysis (CFA).
When TypeScript analyzes this:
ts
function process(value: string | number) {
if (typeof value === "string") {
value.toUpperCase();
}
}
Internally:
- A Control Flow Graph (CFG) is created.
- Each branch refines possible types.
- The compiler intersects original type with the narrowed constraint.
Example: Discriminated Union
`ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}
`
TypeScript understands:
-
kindis a literal discriminator - Each variant is mutually exclusive
- The
ifbranch removes incompatible members
This is not magic — it’s a refinement process.
3️⃣ Conditional Types — The Core Engine
Conditional types are the backbone of advanced type logic.
`ts
type IsString = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString; // false
`
What Actually Happens?
The compiler:
- Substitutes
T - Checks assignability (
T extends U) - Produces one branch
This is type-level branching.
4️⃣ Distributive Conditional Types — Hidden Power
When a naked type parameter appears on the left side of extends, distribution occurs.
`ts
type Wrap = T extends any ? { value: T } : never;
type Result = Wrap;
`
Expands into:
ts
{ value: string } | { value: number }
Why?
Because unions are treated element-wise when T is “naked”.
Prevent Distribution
ts
type Wrap<T> = [T] extends [any] ? { value: T } : never;
Wrapping in a tuple disables distribution.
5️⃣ The infer Keyword — Type Pattern Matching
infer allows extracting types during conditional matching.
Extract Return Type
ts
type GetReturn<T> =
T extends (...args: any[]) => infer R ? R : never;
Extract First Argument
ts
type FirstArg<T> =
T extends (arg: infer A, ...rest: any[]) => any ? A : never;
Deep Extraction Example
ts
type UnwrapPromise<T> =
T extends Promise<infer U> ? U : T;
This is essentially pattern matching on types.
6️⃣ Mapped Types — Iteration at Type Level
Mapped types iterate over keys.
ts
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
Mechanism:
-
keyof Tbecomes a union of keys -
[K in ...]iterates key-by-key - A new type is constructed
7️⃣ Key Remapping & Property Filtering
Since TypeScript 4.1:
`ts
type Prefix = {
};
`
Filtering Keys
`ts
type RemoveId = {
};
`
Returning never removes the key entirely.
8️⃣ Template Literal Types — Compile-Time String Engine
tson${Capitalize}`;
type EventName<T extends string> =
type E = EventName<"click">; // "onClick"
`
Cartesian Explosion
`ts
type Lang = "en" | "ar";
type Key = "title" | "desc";
type TranslationKey = ${Lang}_${Key};
`
Expands into 4 combinations.
⚠️ This can grow exponentially with big unions.
9️⃣ Recursive Types — Type-Level Recursion
Example: DeepReadonly
ts
type DeepReadonly<T> =
T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
What’s happening?
- Checks if
Tis object - Recursively transforms properties
- Stops at primitives
⚠️ Recursion depth is limited (often ~50 instantiations, depends on context).
🔟 Building Advanced Custom Utility Types
DeepPartial
ts
type DeepPartial<T> =
T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
Keys By Value Type
ts
type KeysByValue<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
Indexing collapses the mapped results into a union.
Flatten Nested Object (Caution: can be expensive)
ts
type Flatten<T> = {
T[K] extends object ? Flatten<T[K]> : T[K];
};
1️⃣1️⃣ Reverse Engineering DefinitelyTyped
Example inspired by React:
ts
type ComponentProps<T> =
T extends React.JSXElementConstructor<infer P>
? P
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: {};
What’s happening?
- If it’s a component → extract props via
infer - If intrinsic element → lookup built-in props
- Otherwise →
{}
DefinitelyTyped heavily uses:
- Deep conditional nesting
-
inferextraction - Mapped transformations
- Distributive tricks
- Intersections to merge constraints
1️⃣2️⃣ Variance — The Hidden Foundation
Variance defines how subtyping behaves with generics.
There are 4 flavors:
- Covariant: preserves subtype direction
- Contravariant: reverses subtype direction
- Bivariant: both directions (often unsafe)
- Invariant: neither direction
Covariance Example
`ts
type Box = { value: T };
type A = Box;
type B = Box;
const a: A = { value: "x" };
const b: B = a; // ✅
`
Contravariance (Function Parameters)
`ts
type Fn = (value: T) => void;
let fn1: Fn;
let fn2: Fn;
fn2 = fn1; // ✅
fn1 = fn2; // ❌ unsafe
`
Why? Because if fn1 expects string | number, assigning a function that only accepts string can break.
Bivariance (Historical Loophole)
Methods on object types can behave bivariantly for compatibility:
ts
type Handler<T> = {
handle(value: T): void;
};
This can be convenient, but also a source of unsoundness.
1️⃣3️⃣ Union to Intersection — The “Variance Hack” Explained
This classic utility works because of function parameter contravariance.
ts
type UnionToIntersection<U> =
(U extends any ? (arg: U) => void : never) extends
(arg: infer I) => void
? I
: never;
What’s happening step-by-step?
Given:
ts
type U = { a: string } | { b: number };
Step 1: Distribute into functions
ts
U extends any ? (arg: U) => void : never
Becomes:
ts
(arg: { a: string }) => void
| (arg: { b: number }) => void
Step 2: Infer the parameter
Because parameters are contravariant, inference collapses into an intersection:
ts
(arg: infer I) => void
Result:
ts
{ a: string } & { b: number }
This is a deliberate “exploit” of variance mechanics.
1️⃣4️⃣ Higher-Order Types & Type-Level Function Composition
TypeScript doesn’t support true Higher-Kinded Types (HKTs), but we can simulate them.
Apply — Calling a Type-Level Function
ts
type Apply<F, T> =
F extends { type: (arg: T) => infer R } ? R : never;
Defining a “type-level function”
`ts
type ToArray = {
type: (arg: T) => T[];
};
type R = Apply; // string[]
`
Type-Level Composition (“Piping”)
ts
type Pipe<A, B> =
B extends (arg: infer T) => any
? A extends (arg: any) => T
? (arg: Parameters<A>[0]) => ReturnType<B>
: never
: never;
This is compile-time plumbing: connecting one function type to another.
1️⃣5️⃣ Advanced infer Tricks (Tuples, Promises, and More)
Head / Tail of a Tuple
`ts
type Head =
T extends [infer H, ...any[]] ? H : never;
type Tail =
T extends [any, ...infer R] ? R : never;
`
Reverse a Tuple (Recursive)
ts
type Reverse<T extends any[]> =
T extends [infer H, ...infer R]
? [...Reverse<R>, H]
: [];
Extract Promise Inner Type
ts
type AwaitedLike<T> =
T extends Promise<infer U> ? U : T;
1️⃣6️⃣ Type-Level Parsing (String Manipulation)
Template literal types let us parse strings.
Extract Route Params
ts${string}:${infer Param}/${infer Rest}
type ExtractParams<T extends string> =
T extends${string}:${infer Param}`
? Param | ExtractParams<Rest>
: T extends
? Param
: never;
type Params = ExtractParams<"/user/:id/post/:postId">;
// "id" | "postId"
`
This is a real type-level parser.
1️⃣7️⃣ Building a Fully Type-Safe Event System
Define events:
ts
type EventMap = {
click: { x: number; y: number };
login: { userId: string };
logout: void;
};
Listener:
ts
type Listener<E extends keyof EventMap> =
(payload: EventMap[E]) => void;
Emitter:
`ts
class Emitter {
on(event: E, listener: Listener) {}
emit(event: E, payload: EventMap[E]) {}
}
`
Usage:
`ts
const emitter = new Emitter();
emitter.emit("click", { x: 10, y: 20 }); // ✅
emitter.emit("click", { userId: "1" }); // ❌
`
Powered by:
-
keyofconstraints - indexed access
EventMap[E] - literal inference
1️⃣8️⃣ Exhaustiveness Checking with never
ts
function assertNever(x: never): never {
throw new Error("Unexpected value");
}
`ts
type Shape =
| { kind: "circle" }
| { kind: "square" };
function handle(shape: Shape) {
switch (shape.kind) {
case "circle":
return;
case "square":
return;
default:
assertNever(shape); // ✅ compiler error if a case is missing
}
}
`
1️⃣9️⃣ Compiler Limits — What Actually Breaks
TypeScript has:
- Instantiation depth limits
- Union size limits
- Memory thresholds
Extremely Dangerous Pattern (Union Explosion)
ts
type Explode<T> =
T extends any ? { a: T } | { b: T } : never;
Large unions here can grow exponentially.
2️⃣0️⃣ Performance Impact of Complex Types
TypeScript types are evaluated during compilation.
Heavy constructs increase:
- Type instantiation count
- Union expansion
- Recursive resolution depth
- IDE memory usage
Common Errors
txt
Type instantiation is excessively deep and possibly infinite.
txt
Expression produces a union type that is too complex to represent.
What Causes Slowness?
- Deep recursive mapped types
- Large distributive conditional chains
- Template literal cartesian explosions
- Intersections over wide unions
2️⃣1️⃣ Practical Guidelines for Production Systems
✅ Prefer Shallow Utilities
Avoid deep recursion unless necessary.
✅ Avoid Naked Distribution When Not Needed
Wrap generics in tuples:
ts
[T] extends [any] ? ... : ...
✅ Break Complex Types Into Steps
Instead of:
ts
type MegaType<T> = Deep<Transform<Extract<T>>>;
Do:
ts
type Step1<T> = Extract<T>;
type Step2<T> = Transform<T>;
type Step3<T> = Deep<T>;
type MegaType<T> = Step3<Step2<Step1<T>>>;
✅ Profile Large Codebases
Use:
bash
tsc --extendedDiagnostics
🧠 Final Thoughts
TypeScript’s type system is no longer just static annotations.
It is:
- A functional programming system
- A compile-time evaluator
- A pattern-matching engine
- A string manipulation engine
- A transformation DSL
But with great power comes:
- Complexity
- Compilation cost
- Maintenance challenges
Mastering it allows you to build libraries at the level of:
- React
- Redux Toolkit
- Zod
- tRPC
- Prisma

Top comments (0)