Advanced Types
TypeScript's type system is not a table of annotations but a small pure functional language that executes inside the compiler during checking and is erased entirely before a single byte of JavaScript is emitted. Advanced types are programming in that language: keyof takes a type's keys, a mapped type iterates them and builds a new type, a conditional type is an if over assignability, infer captures part of a pattern, and a recursive alias calls itself. None of it exists at runtime: there is no reflection over types, and as is an unbacked promise to the compiler, not a check. Keep that thesis in mind — half the traps in the topic grow from expecting runtime semantics from a purely compile-time machine.
The other half are the places where the type-level language behaves against intuition, and they are worth naming upfront. keyof on a type with an index signature yields string | number, not string, because JavaScript coerces numeric keys to strings. A conditional type distributes over a union only when the checked type is a naked type parameter; wrapping it in a tuple, [T] extends [U], turns distribution off — and that distinction is what Exclude, NonNullable, and a correct IsNever are built on. infer is legal only inside an extends clause, nowhere else. Interpolating unions into a template literal type expands to the cross product — a fast way to blow the instantiation budget. And recursion hits a hard depth limit (ts(2589), "excessively deep") that TS 4.5 raised from ~50 to ~1000 iterations, but only for recursion in tail position. The layer-by-layer breakdown is below.
Topic map
- The keyof operator and indexed access —
keyof Tas a union of key names,T[K]for safe lookup, and why an index signature yieldsstring | number. - Mapped types — iterating a key union
{ [K in keyof T]: … }, thereadonly/?modifiers, key remapping withas, and homomorphism. - Conditional types —
T extends U ? X : Yas anifover assignability, deferred evaluation, and the definitions ofExclude/Extract/ReturnType. - The infer keyword — capturing part of a pattern inside
extends,infer X extends Yfrom TS 4.7, and union versus intersection across several positions. - Distributive conditionals — distribution over a naked union parameter, suppression with
[T] extends [U], and the distribute-over-nevertrap. - Template literal types — composing string literal types, the cross product of interpolated unions,
Uppercase/Capitalize, and parsing a string withinfer. - Recursive types — self-referencing aliases, tail recursion over tuples, and the compiler's instantiation-depth limit.
Common Mistakes and Traps
| Mistake | Consequence | |
|---|---|---|
Thinking keyof on a type with an index signature yields only string | It is `string | number — JavaScript coerces numeric keys to strings, so number` is a member too |
Expecting keyof to see keys added at runtime | keyof sees only declared keys; that is why Object.keys is typed string[], not (keyof T)[] — a deliberate concession | |
| Thinking a mapped type emits runtime code | It is a compile-time type; Readonly<T> freezes nothing — Object.freeze plays no part | |
Assuming a homomorphic mapping resets readonly/? | A mapping over keyof T preserves T's modifiers; only the non-homomorphic [K in SomeUnion] drops them | |
Reading extends in a conditional as class inheritance | It is an assignability test, not inheritance; 'a' extends string is true | |
| Expecting distribution when the checked type is not naked | Distribution happens only for a naked union parameter; [T] extends [U] turns it off | |
Forgetting distribution over never | never is the empty union: a distributive conditional yields never in zero iterations — which breaks a naive IsNever | |
Using infer outside an extends clause | infer is legal only inside a conditional's extends — anywhere else is an error | |
| Expecting a template literal type to give a runtime string | It is a set of string literal types; interpolating unions gives the cross product | |
| Not noticing the combinatorial blow-up in a template literal type | Two unions of N and M members give N×M members — a fast way to hit the instantiation limit | |
| Putting the recursive call somewhere other than tail position | Before the tail optimisation the depth is ~50 → ts(2589); the tail form (TS 4.5) reaches ~1000 iterations | |
| Reading "excessively deep" as a genuinely infinite type | It is an exhausted depth budget, not infinity; the fix is tail recursion, an accumulator, or a counter | |
Expecting higher-kinded types (F<_>) | TypeScript has none — a type parameter always denotes a concrete type; "any container" is emulated with a string-keyed registry | |
Swapping an unreadable type for an as cast | as is unchecked at runtime and trades a readability problem for a soundness one |
Interview relevance
Advanced types are the domain's senior signal. The interviewer is not checking that you know the words "mapped" and "conditional" but that you hold an execution model of the type-level language: what the compiler computes, in what order, and where its limits are. The middle/senior divide is one question: "when does a conditional type distribute over a union, and how do you stop it?" The right answer is not "always" but "only when the checked type is a naked type parameter; [T] extends [U] turns distribution off." The same move applies across the topic: you are asked to rebuild ReturnType with infer, explain why Awaited must recurse, and fix a type failing with "excessively deep" by naming the real cause — the position of the recursive call.
What they usually check:
- What
keyof Tyields and why an index signature widens it tostring | number. - How to write
Partial,Required,Readonlyby hand and what anasclause does in a mapped type. - What
T extends U ? X : Ymeans, that it is an assignability test, and that the type is erased from the output. - Where
inferis legal and howReturnTypeis defined through it. - When a conditional type distributes over a union and how to turn distribution off.
- What a template literal type can express and why interpolating unions causes a combinatorial blow-up.
- How to type a recursive
Json, why the depth limit arises, and how tail recursion raises it.
Typical wrong answer: "a conditional type always distributes over any union it meets." A candidate who believes this cannot explain why [T] extends [never] ? true : false is the only correct way to write IsNever, while the naive T extends never ? true : false always yields never (distribution over the empty union is zero iterations). The second classic failure is reading "Type instantiation is excessively deep" as a sign the type is infinite and "fixing" it by raising the compiler's memory. In reality it is an exhausted instantiation-depth budget: a recursive call baked inside a template literal keeps every frame live on the stack at once, and moving it into tail position with an accumulator lets the compiler unroll the recursion iteratively.