| |

TypeScript 46 ๐Ÿ”ท Type-Level Programming

At some point TypeScript stops feeling like a language for describing JavaScript and starts feeling like a language in its own right โ€” one that runs at compile time, manipulates types instead of values, and produces errors instead of output. This is type-level programming: using the type system as a computational substrate. Conditional types are the if. Mapped types are the for loops. Recursive types are recursion. infer is pattern matching. Template literal types are string manipulation. Put together, they let you write utilities that compute new types from old ones, validate string formats at compile time, transform object shapes, and enforce invariants that would otherwise require runtime checks. This chapter is about that discipline โ€” the tools, the idioms, and the limits. It is not a catalog of tricks but a guide to thinking in types.

Key point: Type-level programming uses the type system’s own constructs as a programming language. Conditional types (T extends U ? X : Y) are branching. Mapped types ({ [K in keyof T]: ... }) are iteration. infer extracts types from patterns. Template literal types manipulate strings. Recursion is allowed but depth-limited. The tools compose, but each layer of abstraction adds compile-time cost and reduces readability. The goal is not to write the most clever type โ€” it is to write the simplest type that enforces the invariant you need.


The building blocks

Type-level programming is built from a small set of constructs that were introduced across several TypeScript versions. Each is simple on its own, but they compose into a powerful system.

Conditional types are the branching construct. T extends U ? X : Y evaluates to X if T is assignable to U, and Y otherwise. They were added in TypeScript 2.8 and are the foundation of most type-level logic.

Mapped types are the iteration construct. { [K in keyof T]: F<T[K]> } produces a new object type by applying F to each property of T. They were added in TypeScript 2.1 and are how utilities like Partial, Readonly, and Pick are built.

infer is the extraction construct. It appears inside a conditional type’s extends clause and introduces a type variable that TypeScript infers from the pattern. T extends (infer U)[] ? U : never extracts the element type of an array.

Template literal types are the string manipulation construct. `prefix-${T}` creates a new string literal type by substituting T into a template. They were added in TypeScript 4.1 and enabled a new class of type-level string processing.

Recursive types are how type-level computation repeats. A type can refer to itself, subject to the depth limit discussed in TypeScript 43. Recursion plus conditional types is how loops are written at the type level.

Why these compose. Each construct is a primitive, and the power comes from combining them. A conditional type can use infer to extract a piece of a type and then recursively apply itself to that piece. A mapped type can use a conditional type to transform each property. A template literal type can be used as a pattern in a conditional type to match string shapes. The vocabulary is small, but the combinations are extensive.

Why this is worth learning even if you never write a complex type. The utility types that TypeScript ships with โ€” Partial, Required, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, Awaited โ€” are all written with these constructs. Reading their definitions is how you understand what they do and when they are the wrong tool. And the errors the compiler produces when a complex type fails are in this vocabulary. Knowing the vocabulary means being able to read the error.


Conditional types as branching

The conditional type is the most fundamental tool. Its form is T extends U ? X : Y, and it answers the question “is T assignable to U?”

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<"hello">;  // true โ€” "hello" is assignable to string

The conditional type does not just answer true or false. It can return different types for the two branches, which is where the power lies. type UnwrapArray<T> = T extends (infer U)[] ? U : T returns the element type if T is an array, and T itself otherwise.

Why assignability, not identity. The check is “is T assignable to U,” not “is T exactly U.” "hello" is assignable to string, so IsString<"hello"> is true. This is usually what is wanted, but it means the conditional type cannot distinguish a literal from its widened form unless you use a different pattern.

The never distribution. When T is a naked type parameter and is instantiated with a union, the conditional type distributes over each member. This is the subject of TypeScript 44, and it is the single most important thing to understand about conditional types. It is why Exclude<T, U> works and why T extends never ? X : Y always evaluates to never.

Distinguishing union from non-union. The check [T] extends [never] uses a tuple to prevent distribution and detect the empty union. The check [T] extends [U] uses the same technique to compare the whole type rather than its members. These idioms are the standard way to control distribution when the default behavior is wrong for the task.


Mapped types as iteration

A mapped type iterates over the keys of an object type and produces a new object type with transformed values. The syntax is { [K in Keys]: ValueType }.

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };

MyPartial makes every property optional, MyReadonly makes every property readonly, and MyRequired removes optionality. The ? adds optionality, -? removes it, readonly adds the modifier, and -readonly removes it. The keyof T constraint means the mapped type iterates over exactly the keys of T.

Why the modifiers are significant. A mapped type can add, remove, or preserve the readonly and optional modifiers on each property. This is what makes Partial and Required inverses of each other โ€” one adds ?, the other removes it. The modifier syntax is the mechanism.

Key remapping with as. TypeScript 4.1 added the ability to transform keys during the mapping. The syntax is { [K in keyof T as NewKey]: T[K] }, where NewKey is computed from K.

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

The as clause uses a template literal type to construct new keys from the original ones. Keys that map to never are removed entirely, which is how filtering is done at the type level.

Why key remapping is powerful. It turns a mapped type from a simple transformation into a key-space manipulator. Combined with conditional types, it lets you rename, filter, and reshape keys in ways that would otherwise require a separate type for each variant. This is the technique behind types like Omit (Pick<T, Exclude<keyof T, K>>) and many library utilities.


infer as pattern matching

The infer keyword introduces a type variable inside a conditional type’s extends clause, and TypeScript fills it in by matching the pattern.

type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>;   // string
type B = ElementType<number[]>;   // number

The pattern (infer U)[] matches any array type, and U is inferred as the element type. The pattern can be more elaborate: T extends Promise<infer U> ? U : never extracts the resolved type of a promise, and T extends (...args: infer A) => infer R ? [A, R] : never extracts both the parameter list and the return type of a function.

Why infer is pattern matching. It is the type-level equivalent of destructuring: you describe the shape you expect, and the compiler extracts the parts that vary. Multiple infer positions in the same pattern extract multiple pieces, and the pattern can be as specific as needed.

The infer X extends Y syntax. TypeScript 4.7 added the ability to constrain an inferred type: T extends (infer U extends string)[] ? U : never only matches arrays of strings. This is useful when the pattern is ambiguous and you need to narrow it.

Why the last infer in a pattern wins. In a pattern with multiple infer positions, the compiler takes the last one for a given variable. This is rarely a problem with well-structured patterns, but it is a detail to know when debugging unexpected inference.


Template literal types

Template literal types manipulate strings at the type level. They use the same syntax as JavaScript template literals, but the substitutions are types rather than values.

type Greeting = `Hello, ${string}!`;

const a: Greeting = "Hello, world!";   // โœ…
// const b: Greeting = "Hi, world!";   // โŒ

Greeting matches any string of the form “Hello, ” followed by anything, followed by “!”. This is pattern matching over strings, and it is the foundation of type-level string processing.

The intrinsic utility types. TypeScript provides four intrinsic string manipulation types: Uppercase<S>, Lowercase<S>, Capitalize<S>, and Uncapitalize<S>. They operate on string literal types.

type A = Uppercase<"hello">;        // "HELLO"
type B = Capitalize<"hello">;       // "Hello"
type C = Uncapitalize<"Hello">;     // "hello"

These are used inside mapped types to transform property names โ€” the Getters example above is the canonical case.

String splitting and joining. Template literal types can be used with infer to split strings at delimiters.

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"]

The pattern `${infer Head}${D}${infer Tail}` matches a string that contains the delimiter, splitting it into the part before and the part after. Recursion handles multiple delimiters. This is how types like CamelCase and KebabCase conversions are implemented.

Why template literal types matter for validation. A type like `v${number}` matches any version string. A type like `${number}px` matches CSS lengths. The compiler can reject invalid strings before they reach a runtime check, which is a real class of bug eliminated at the type level.


Putting it together

The constructs compose. A realistic example: a type that validates and extracts route parameters.

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }

The type walks the string, finds each :param segment, and accumulates the parameter names into an object type. It uses conditional types, template literal types, infer, recursion, and mapped types โ€” all five constructs in a single utility. This is type-level programming in practice, and it is the technique behind router libraries that type their route parameters.

Why this example is realistic and not contrived. Typed route parameters are a real feature in modern Angular and React routers. The type is computed from the route string at compile time, and the component receives a typed params object. Without type-level programming, the parameter names would be strings and the access would be untyped. With it, the compiler catches typos in parameter names and infers the shape of the params object.

Why this example also shows the cost. The type is dense. Reading it requires understanding all five constructs. Debugging it requires understanding the compiler’s evaluation order. A change to the route syntax requires updating the type. This is the tradeoff of type-level programming: the runtime code becomes simpler and safer, and the type becomes harder to read. Whether the trade is worth it depends on how much the type protects and how often the type changes.

Why a simpler type is often better. A type that is slightly less precise but readable is often the better choice. If a route has three parameters and they never change, writing the params interface by hand is clearer than computing it from the route string. Type-level programming is for cases where the computation is necessary โ€” where the type genuinely must be derived, or where it changes often enough that maintaining it by hand is worse.


Limits and costs

Type-level programming has real limits, and respecting them is part of using it well.

Depth limits. Recursive types are depth-limited, and the limit is not generous. A type that recurses 50 times may hit the limit on some TypeScript versions. Deeply nested data or long strings can exceed it. The error “Type instantiation is excessively deep and possibly infinite” is the signal.

Compilation cost. Every conditional type, mapped type, and recursive call is work the compiler has to do. A few well-placed types are cheap. A large library of deeply nested types applied to big object types can slow the editor and the build noticeably. The cost is not linear โ€” it compounds across files and across tsc runs.

Readability. Type-level code is not easy to read, even for people who wrote it. The vocabulary is unfamiliar, the errors are obscure, and the intent is often hidden behind the mechanics. A type that saves a runtime check but requires an hour to understand is not obviously a win.

Debugging difficulty. When a type-level program produces the wrong result, the error message is often a type that is almost right but not quite, with no clear indication of which step went wrong. Techniques like breaking a type into named intermediate types help, because the error then points at the step whose output is wrong.

Why the simplest type that works is the right default. A type that is one line long and enforces the necessary invariant beats a type that is twenty lines and enforces the same invariant plus three others that never come up. The invariant is the goal; the type is the mechanism. Choose the mechanism that is adequate to the goal.


Complete Example Session

// ============================================
// PART 1: CONDITIONAL TYPES
// ============================================

type IsArray<T> = T extends unknown[] ? true : false;

type A = IsArray<string[]>;   // true
type B = IsArray<string>;     // false

// ============================================
// PART 2: MAPPED TYPES
// ============================================

type MyPartial<T> = { [K in keyof T]?: T[K] };

interface User {
  id: string;
  name: string;
  email: string;
}

type PartialUser = MyPartial<User>;
// { id?: string; name?: string; email?: string }

// ============================================
// PART 3: KEY REMAPPING
// ============================================

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getId: () => string; getName: () => string; getEmail: () => string }

// ============================================
// PART 4: INFER
// ============================================

type ElementType<T> = T extends (infer U)[] ? U : never;

type C = ElementType<string[]>;   // string
type D = ElementType<number[]>;   // number

type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;

type E = ReturnOf<() => string>;  // string

// ============================================
// PART 5: TEMPLATE LITERAL TYPES
// ============================================

type EventName = `on${Capitalize<string>}`;

const valid: EventName = "onClick";   // โœ…
// const invalid: EventName = "click"; // โŒ

// ============================================
// PART 6: RECURSION
// ============================================

type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface Config {
  server: { host: string; port: number };
}

type ReadonlyConfig = DeepReadonly<Config>;
// { readonly server: { readonly host: string; readonly port: number } }

// ============================================
// PART 7: STRING SPLITTING
// ============================================

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"]

// ============================================
// PART 8: PUTTING IT TOGETHER
// ============================================

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }

// ============================================
// PART 9: DEBUGGING WITH INTERMEDIATE TYPES
// ============================================

// Break a complex type into named steps:
type Step1<T> = T extends `${string}:${infer P}/${infer R}` ? [P, R] : never;
type Step2<T> = T extends [infer P, infer R] ? { [K in P & string]: string } : never;

// The error then points at the step that produced the wrong output.

// ============================================
// PART 10: LIMITS
// ============================================

// Deep recursion hits the limit:
// type Infinite<T> = T extends any ? Infinite<T> : never; // โŒ

// Large unions expand combinatorially:
// Distributive conditional types over 1000-member unions are slow.

// The simplest type that works is the right default.

Each part isolates one construct, and part 8 shows them composed. Part 9 shows the debugging technique, and part 10 names the limits.


Quick Reference

The Five Constructs

ConstructSyntaxPurpose
ConditionalT extends U ? X : YBranching
Mapped{ [K in keyof T]: F<T[K]> }Iteration
inferT extends (infer U)[] ? U : neverExtraction
Template literal`prefix-${T}`String manipulation
RecursionSelf-referenceLoops

Mapped Type Modifiers

ModifierEffect
?Add optional
-?Remove optional
readonlyAdd readonly
-readonlyRemove readonly
as NewKeyRemap key

String Utilities

UtilityExample
Uppercase<S>"HELLO"
Lowercase<S>"hello"
Capitalize<S>"Hello"
Uncapitalize<S>"hello"

Common Patterns

PatternPurpose
T extends (infer U)[] ? U : neverElement type
T extends Promise<infer U> ? U : neverResolved type
T extends (...a: infer A) => infer RParams and return
{ [K in keyof T]: F<T[K]> }Transform properties
[T] extends [never]Detect never

Limits

LimitEffect
Depth~50 for conditional types
Union sizeCombinatorial expansion
Compile timeCompounds with nesting
ReadabilityDense and indirect
DebuggingErrors are indirect

Best Practices

โœ… Do This:

// Use named intermediate types for complex logic
type Step1<T> = T extends `${infer A}/${infer B}` ? [A, B] : never;
type Step2<T> = Step1<T> extends [infer A, infer B] ? A | B : never; // โœ…

// Use readonly to make covariance sound
type Immutable<T> = { readonly [K in keyof T]: T[K] };          // โœ…

// Use key remapping for property transformations
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }; // โœ…

// Break recursion with a base case
type Flatten<T> = T extends readonly (infer U)[]
  ? U extends readonly unknown[] ? Flatten<U> : U
  : T;                                                          // โœ…

// Prefer a hand-written type when it is simpler
interface RouteParams { userId: string; postId: string; }        // โœ…

โŒ Don’t Do This:

// Don't recurse without a base case
// type Infinite<T> = T extends any ? Infinite<T> : never;       // โš ๏ธ

// Don't distribute over large unions
// type Transform<T> = T extends string ? F<T> : never;          // โš ๏ธ over 1000 members

// Don't write a type you cannot debug
// Break it into named steps first.                              // โš ๏ธ

// Don't compute what you can declare
// If the shape is fixed, write it out.                          // โš ๏ธ

// Don't forget the tuple wrapper when testing whole unions
type WrongIsNever<T> = T extends never ? true : false;           // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Non-terminating recursionDepth limit errorAdd a base case
Distribution over unionsUnexpected member-by-memberWrap in [T]
T extends neverAlways neverUse [T] extends [never]
Large union in conditionalSlow compilationBound the union size
Unreadable typeHard to maintainNamed intermediate types
infer in wrong positionNo inferenceMatch the pattern precisely
Key remapping to neverKey removedIntended for filtering
Modifier accumulationWrong optionalityUse -? to remove

Real-World Examples

1. Partial

type Partial<T> = { [K in keyof T]?: T[K] };

2. Readonly

type Readonly<T> = { readonly [K in keyof T]: T[K] };

3. ReturnType

type ReturnType<T> = T extends (...a: any[]) => infer R ? R : never;

4. Parameters

type Parameters<T> = T extends (...a: infer P) => any ? P : never;

5. Awaited

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

6. Route parameter extraction

type ExtractParams<T extends string> = /* ... */;

7. Event handler names

type EventName<T extends string> = `on${Capitalize<T>}`;

8. Deep partial

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

9. Union to tuple

type UnionToTuple<T> = /* ... */;

10. CamelCase to KebabCase

type KebabCase<S extends string> = /* ... */;

Visual: The Constructs

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CONDITIONAL        T extends U ? X : Y                  โ”‚
โ”‚                     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                โ”‚
โ”‚                     Branching โ€” the if statement         โ”‚
โ”‚                                                          โ”‚
โ”‚  MAPPED             { [K in keyof T]: F<T[K]> }          โ”‚
โ”‚                     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€           โ”‚
โ”‚                     Iteration โ€” the for loop             โ”‚
โ”‚                                                          โ”‚
โ”‚  INFER              T extends (infer U)[] ? U : never    โ”‚
โ”‚                     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€         โ”‚
โ”‚                     Extraction โ€” pattern matching        โ”‚
โ”‚                                                          โ”‚
โ”‚  TEMPLATE LITERAL   `prefix-${T}`                        โ”‚
โ”‚                     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                        โ”‚
โ”‚                     String manipulation                  โ”‚
โ”‚                                                          โ”‚
โ”‚  RECURSION          type F<T> = ... F<...> ...           โ”‚
โ”‚                     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€              โ”‚
โ”‚                     Loops โ€” bounded by depth limit       โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Composing Constructs

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ExtractParams<"/users/:userId/posts/:postId">           โ”‚
โ”‚                                                          โ”‚
โ”‚  Step 1: Template literal match                          โ”‚
โ”‚    `${string}:${infer Param}/${infer Rest}`              โ”‚
โ”‚    Param = "userId"                                      โ”‚
โ”‚    Rest  = "posts/:postId"                               โ”‚
โ”‚                                                          โ”‚
โ”‚  Step 2: Recurse on `/${Rest}`                           โ”‚
โ”‚    ExtractParams<"/posts/:postId">                       โ”‚
โ”‚    Param = "postId"                                      โ”‚
โ”‚    Rest  = ""                                            โ”‚
โ”‚                                                          โ”‚
โ”‚  Step 3: Mapped type accumulates keys                    โ”‚
โ”‚    { userId: string; postId: string }                    โ”‚
โ”‚                                                          โ”‚
โ”‚  Five constructs, one utility.                           โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: When to Use Type-Level Programming

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Is the type computed from other types?                   โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”œโ”€โ”€ No  โ”€โ”€โ–บ Write it by hand                        โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ””โ”€โ”€ Yes                                             โ”‚
โ”‚            โ”‚                                              โ”‚
โ”‚            โ”œโ”€โ”€ Is the computation simple (one or two      โ”‚
โ”‚            โ”‚   constructs)?                               โ”‚
โ”‚            โ”‚      โ”‚                                       โ”‚
โ”‚            โ”‚      โ””โ”€โ”€ Yes โ”€โ”€โ–บ Write the type              โ”‚
โ”‚            โ”‚                                              โ”‚
โ”‚            โ””โ”€โ”€ Is the computation complex (three or more  โ”‚
โ”‚                constructs, recursion)?                    โ”‚
โ”‚                   โ”‚                                       โ”‚
โ”‚                   โ”œโ”€โ”€ Does it change often?               โ”‚
โ”‚                   โ”‚      โ”‚                                โ”‚
โ”‚                   โ”‚      โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Compute the type     โ”‚
โ”‚                   โ”‚      โ”‚                                โ”‚
โ”‚                   โ”‚      โ””โ”€โ”€ No  โ”€โ”€โ–บ Write it by hand     โ”‚
โ”‚                   โ”‚                                       โ”‚
โ”‚                   โ””โ”€โ”€ Will others read it?                โ”‚
โ”‚                          โ”‚                                โ”‚
โ”‚                          โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Document or simplify โ”‚
โ”‚                          โ”‚                                โ”‚
โ”‚                          โ””โ”€โ”€ No  โ”€โ”€โ–บ Compute the type     โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Cost vs Value

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Value                                                   โ”‚
โ”‚    โ–ฒ                                                     โ”‚
โ”‚    โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                           โ”‚
โ”‚    โ”‚         โ”‚  Sweet spot   โ”‚                           โ”‚
โ”‚    โ”‚         โ”‚  Real invariantโ”‚                          โ”‚
โ”‚    โ”‚         โ”‚  Real benefit โ”‚                           โ”‚
โ”‚    โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                           โ”‚
โ”‚    โ”‚                                                     โ”‚
โ”‚    โ”‚  โ”Œโ”€โ”€โ”€โ”                                              โ”‚
โ”‚    โ”‚  โ”‚   โ”‚  Trivial types                             โ”‚
โ”‚    โ”‚  โ””โ”€โ”€โ”€โ”˜  (no benefit)                                โ”‚
โ”‚    โ”‚                                                     โ”‚
โ”‚    โ”‚                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚
โ”‚    โ”‚                          โ”‚ Overkill โ”‚              โ”‚
โ”‚    โ”‚                          โ”‚ Unreadableโ”‚             โ”‚
โ”‚    โ”‚                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚
โ”‚    โ”‚                                                     โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Cost     โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConstructRoleExample
ConditionalBranchT extends U ? X : Y
MappedIterate{ [K in keyof T]: F<T[K]> }
inferExtractT extends (infer U)[] ? U : never
Template literalStrings`prefix-${T}`
RecursionLooptype F<T> = ... F<...>
Key remappingRename[K in keyof T as NewKey]

Key takeaways:

  • Type-level programming treats the type system as a computational language โ€” conditional types branch, mapped types iterate, infer extracts, template literals manipulate strings, and recursion repeats
  • The constructs compose โ€” a single utility can use all five, and the resulting type is computed from its input at compile time
  • Conditional types distribute over unions unless the parameter is wrapped in a tuple, and this is the single most important behavior to understand
  • Mapped types transform object shapes and can remap keys with as, which is how filtering and renaming are done at the type level
  • infer is pattern matching โ€” it extracts the varying parts from a type pattern, and multiple infer positions extract multiple pieces
  • Template literal types enable string processing at the type level, which is how typed route parameters and event names are built
  • Recursion is allowed but depth-limited, and non-terminating recursion is caught by the compiler’s depth limit
  • The cost is real โ€” compilation time, readability, and debugging difficulty all increase with complexity
  • The simplest type that enforces the invariant is the right default โ€” a hand-written interface beats a computed type when the shape is fixed
  • Named intermediate types make complex types debuggable โ€” the error points at the step whose output is wrong

Remember: Type-level programming is a tool for encoding invariants that runtime cannot check. It is powerful, and it is easy to overuse. The question to ask before writing a complex type is not “can I compute this?” but “does computing this catch a bug that a simpler type would miss?” When the answer is yes, the constructs are there: conditional, mapped, infer, template literal, and recursion. When the answer is no, a plain interface is faster, clearer, and just as correct.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!