Language Internals
Part 5 of 6 · JS/TS Language ProficiencyTypeScript Type System — Generics, Conditional Types, Mapped Types & Inference
Structural typing, generics, conditional/mapped types, inference patterns.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does structural typing check?
Answer
That the value has the required members. The type's name is not identity.
L2
When do excess property checks apply?
Answer
On a fresh object literal assigned to a narrower type. A variable that already has a wider type generally passes.
L3
Where does TypeScript infer a type argument?
Answer
From arguments, from the expected return context, and from contextual typing of callbacks. NoInfer blocks an inference site you want to keep explicit.
L4
What does a naked type parameter do in a conditional?
Answer
It distributes. A union input becomes a union of results, one branch per member.
L5
What does infer pull out?
Answer
A piece of a matched type, such as the element of an array or the return of a function.
L6
Why prefer a discriminated union to optional fields?
Answer
Each variant names the fields it actually has. A switch can be exhaustive. Optional soup allows combinations that cannot occur.
L7
How do you get a nominal id in a structural system?
Answer
Intersect with a brand the caller cannot forge, often a unique symbol on a phantom property.
Failure modes
Optional soup
Every field is optional, so the type accepts states the function will crash on.
any at the boundary
Inference and checking stop. Callers look typed and are not.
Forgetting distribution
A conditional on a naked parameter fans out over a union and the result is wider than the author expected.
Brand forged with a cast
A nominal id helps until someone writes as UserId. The brand is a convention, not a runtime check.
Variance surprise on methods
Method parameters have historically been compared bivariantly. strictFunctionTypes tightens function types that are not methods.
Misconceptions
TypeScript is nominal like Java.
Names do not distinguish types. Shape does, plus excess-property checks on fresh literals.
A conditional type runs in the emitted JavaScript.
It is erased. If you need a runtime branch, write a function. The type only guides the call.
Partial makes nested fields optional.
Partial, Required, Pick, and Omit are shallow. Nested objects need an explicit recursive mapped type.
Interviewer traps
Reaching for a conditional type when a union would do.
If the cases are data, use a discriminant. Save conditionals for 'given this input type, compute that output type'.
Saying extra properties are always rejected.
Fresh literals are checked. An existing variable with extra fields is structurally fine.
Claiming a brand is a runtime guarantee.
The brand disappears at emit. Validate at the boundary if the id must be real.
Design scenario
Same prompt for every reader.
Requirements
Type the handler from the event name, keep the runtime as a plain function, and say what still must be checked when JSON arrives.
Traffic / scale
The type is compile-time. Runtime traffic is ordinary event delivery on one realm.
Latency
The checker runs in the editor and in CI. It does not sit on the request path.
Consistency
A handler for data must accept a payload with id. A handler for ready must not require one.
Availability
A bad compile should fail the build. A bad JSON payload still needs a runtime parse because types were erased.
Failure assumptions
- Callers will rely on inference.
- Unions of event names must not collapse into one optional payload.
- Emit strips the types. The next page owns how the declarations ship.
Constraints
- Prefer a discriminated union or a mapped handler table.
- Do not propose a runtime reflection library.
Prompt
A client SDK accepts an event name and a handler. ready takes no payload. data requires an id. Callers currently pass a string plus optional fields and find out at runtime.
How should this API exclude impossible states?
Prefer
Discriminant plus generics
A tag selects the payload. A generic or a mapped handler table ties the callback to that tag. Exhaustive switches see a never in the leftover case.
- Each variant carries only its fields.
- Call sites infer the payload from the tag.
- Adding a variant fails the build until handlers catch up.
Alternative
Optional fields on one type
Fast to write. The type allows combinations the function never handles, so the bug moves to runtime.
- Every field might be missing.
- Inference has nothing sharp to grab.
- Reviewers cannot see which combinations are real.
Read a conditional
Distribution happens when the checked type is a naked type parameter.
- 1
Name the input
Often a type parameter, sometimes a concrete type. - 2
Match extends
If the input is a naked parameter, each union member is checked on its own. - 3
Bind with infer
Pull the array element, the return type, or a slice of a string. - 4
Remember erasure
The branch you picked is a type. The JavaScript still needs its own test.
Overview
TypeScript's power is a compile-time language sitting on JavaScript. Generics parameterize it. Inference fills holes from the way you call. Conditional types branch. Mapped types walk keys and rebuild an object type. The default comparison is structural: members matter, names do not, until you add a brand on purpose.
The checker deletes all of this at emit. A green type and a bad JSON payload can still meet at runtime. Parse at the boundary. Use types to make the parsed value honest after that.
Structural typing
type Point = { x: number; y: number };
function norm(p: Point) {
return Math.hypot(p.x, p.y);
}
const q = { x: 3, y: 4, label: "q" };
norm(q); // OK structurally. q is not a fresh literal in the argument slot.A fresh object literal written where a Point is expected would be checked for extra properties. label on a variable that already has a wider type is allowed. The runnable keeps the runtime half: Math.hypot(3, 4) is 5. The excess-property error is a checker result, so it cannot be shown by running JavaScript.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Generics and inference
function identity<T>(x: T): T {
return x;
}
identity(1); // T inferred as number
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
function len<T extends { length: number }>(x: T) {
return x.length;
}Inference sites are the arguments of a call, the expected return type, and contextual typing for callbacks. NoInfer (TypeScript 5.4) marks a type parameter position that should not be guessed from that argument, so a default or an earlier argument wins.
Constraints (T extends { length: number }) reject callers that lack the member and keep the member available inside the function.
Conditional types
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<"hi">; // "yes"
// Distributive when T is a naked type parameter:
type ToArray<T> = T extends any ? T[] : never;
type B = ToArray<string | number>; // string[] | number[]
type Elem<T> = T extends (infer U)[] ? U : never;
type C = Elem<string[]>; // string
type ReturnOf<F> = F extends (...args: never[]) => infer R ? R : never;T extends any is a common way to force distribution. It is easy to do by accident. Wrap the parameter in a tuple ([T] extends [any]) when you want one check against the whole union.
Decisions
- 1
1. Type T
- next2. T extends X?
- ?
2. T extends X?
- yes3. Then branch
- no4. Else branch
- 3
3. Then branch
- 4
4. Else branch
Lesson map
TypeScript Type System — Generics, Conditional Types, Mapped Types & Inference
Structural typing, generics, conditional/mapped types, inference patterns.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB t["1. Type T"] cond["2. T extends X?"] then["3. Then branch"] else["4. Else branch"] t -->|1. Type T to 2. T extends X?| cond cond -->|yes| then cond -->|no| else
If T is a naked parameter and the input is a union, read the diagram once per member and union the results.
Mapped types and key remapping
type Readonlyish<T> = { readonly [K in keyof T]: T[K] };
type Partialish<T> = { [K in keyof T]?: T[K] };
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type IdLabeled = { [K in "a" | "b" as `id_${K}`]: string };Partial, Required, Pick, and Omit are the shallow versions of this idea. Key remapping (TypeScript 4.1) plus template literal types is how route params and event names become types. A recursive mapped type is what you write when "deep readonly" must walk nested objects. The built-in Readonly does not.
Patterns
| Goal | Pattern | Notes |
|---|---|---|
| Optional fields | Partial | Shallow |
| Required fields | Required | Shallow |
| A smaller surface | Pick or Omit | Prefer a small composed type over a huge interface |
| Mutually exclusive props | Discriminated union | Better than optional soup |
| Opaque ids | Brand with a unique symbol | Nominal flavor. Erased at runtime |
| Keep a literal | satisfies and as const | Checks the value and preserves the narrow type |
Function parameters on methods have historically been compared bivariantly, which is looser than a standalone function type. strictFunctionTypes checks non-method functions contravariantly in their parameters. Prefer an explicit generic for a callback when the direction of the data matters. The flag itself is part of strictness and emit.
A small toolbox
The aliases are the lesson. They produce no JavaScript. The on function below is the runtime shape of the same idea: the event name selects the payload.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type EventMap = { ready: void; data: { id: string }; error: Error };
type Handler<E extends keyof EventMap> = EventMap[E] extends void
? () => void
: (payload: EventMap[E]) => void;
declare function on<E extends keyof EventMap>(event: E, handler: Handler<E>): void;
on("data", (p) => console.log(p.id));
// on("data", () => {}); // error — a payload is requiredPress Run. Snippets must be self-contained — no network, files, or native modules.
The checker version of Handler is stricter than this runtime if. That gap is the point of the next page: what you emit is JavaScript, and the .d.ts is a separate artifact.
Interview Q&A
Why prefer a discriminated union over a string type plus optional fields?
Answer
The tag selects one payload. A switch can end in never, so a new variant fails the build. Optional fields allow combinations that cannot happen, and the function finds out by crashing.
What does T extends any do in a conditional?
Answer
When T is a naked type parameter, the conditional distributes over each union member. People use T extends any on purpose to get that fan-out. If you needed one answer for the whole union, wrap T so it is not naked.
How does variance work in TypeScript?
Answer
Assignability of functions is contravariant in parameters under strictFunctionTypes, with a long-standing exception for methods, which are checked bivariantly. Prefer a generic so the callback's argument is the type you named, and turn strictness on for libraries.
Why can I pass an object with an extra field?
Answer
Structural typing. Excess property checks are a freshness rule for object literals, not a nominal wall. If the extra field is a bug, brand the type or parse into a known shape.
What does infer buy you?
Answer
It names a piece of a type you matched: the element of an array, the return of a function, the middle of a template string. It is how utility types unpack something the caller wrote inline.
Is a mapped type a runtime loop?
Answer
No. { [K in keyof T]: ... } runs in the checker. At runtime you still write a function if you need to copy keys. Confusing the two produces types that look thorough and JavaScript that does nothing.
When would you brand a string?
Answer
When UserId and OrderId must not be interchangeable even though both are strings. Intersect with a phantom property. Remember the brand is erased, so a cast or unparsed JSON can still forge it.
What do you say in the first minute?
Answer
Structural by default. Generics plus inference. Conditionals distribute on a naked parameter and infer unpacks a match. Mapped types rebuild keys. Discriminated unions beat optional soup. Types do not survive emit.
Pitfalls
- Using
anybecause a generic felt slow to write. - Expecting
Partialto walk nested objects. - Forgetting that a naked conditional fans out.
- Branding an id and then casting through it at the JSON boundary.
- Writing a conditional where a three-variant union was enough.
- Debugging a type error by reading the emitted JavaScript. The JavaScript does not contain the type.
Model a result as either { ok: true, value: string } or { ok: false, error: string }. Write the switch that uses the tag. Then write the optional-field version and name one object both "types" would have allowed that only the union rejects. Keep the runtime check you would still run on JSON.