Advanced types

Conditional distribution, infer, mapped and template types, recursion, variance, brands, and type equality.

The idea

File 08 described a type layer that describes shapes. This one gives it the ability to compute, and the shift is bigger than the syntax suggests: you stop annotating and start programming.

The trick to reading any of it is to notice that the pieces map onto things you already know:

type-level value-level
A extends B ? X : Y if
{ [K in keyof T]: … } a loop over keys
infer U destructuring — bind a name to a matched part
`${A}-${B}` template strings
a type referring to itself recursion

It is a small pure, lazy, functional language. Pure and lazy matter practically: nothing is evaluated until something asks, so a type that would be an infinite loop is fine as long as nobody instantiates it that deep.

Two operators bridge the two worlds and everything here is built from them: typeof moves a value into type space, and keyof looks inside a type. Indexed access (T["k"], T[number]) is how you read a part back out.

Conditional types, and the behaviour that hides

A extends B ? X : Y is the if. Read extends as “is assignable to”, not “is exactly” — file 37’s whole subject, arriving early.

Then the behaviour that is invisible until it bites. When the checked side is a bare type parameter and the argument is a union, the conditional runs once per member and unions the answers back together. Not once for the whole union — once each. That is called distribution, and it is the reason Exclude works at all.

You turn it off by removing the bareness: wrap both sides in a tuple, [T] extends [string], and the union is tested as a single thing. Being able to do that deliberately in both directions is the skill.

Two consequences worth predicting before you read them:

keyof over a union flips the operation, which sounds wrong until you say it out loud: given “an A or a B”, the only keys you can safely read are the ones both have.

infer is pattern matching

infer U describes a shape and gives a name to the part you want pulled out of it. It only works inside an extends clause, because that is the only place a match is happening.

Once you have it, most of the utility types from file 08 are one line each: unwrap a promise, unwrap an array, take a function’s return type or its parameters. infer U extends string adds a constraint to the match.

The same name matched in several positions combines, and how depends on where:

That asymmetry looks like a quirk and is a tool — it is what makes UnionToIntersection possible at the end of the file.

Mapped types are the loop

{ [K in keyof T]: T[K] } is the base form, and the body of the type is the loop — there is no other member. From there:

One special case earns its own paragraph. Written in exactly the shape { [K in keyof T]: … }, a mapped type is homomorphic: modifiers are preserved automatically, and mapping an array gives back an array rather than an object with push as a property. That is the only reason Partial and Readonly are usable on collections at all.

Template literal types

String manipulation in type space, with four intrinsic helpers — Uppercase, Lowercase, Capitalize, Uncapitalize.

A union in a slot multiplies out against every other slot, so two unions of two give four members and these grow explosively; the checker gives up at 100,000. Combined with infer, templates can take strings apart, which is how typed route parameters and typed object paths are built.

Recursion, and where you put the call

A type may refer to itself as long as the reference sits inside something — an object, array, or tuple. A bare type A = A has no shape to work with and is rejected.

Where the recursive call sits decides how far you get. In tail position — the recursive call is the entire result — you get roughly 1000 levels. Anywhere else, roughly 50. Same logic, twenty times the room, and exceeding either gives error TS2589, “Type instantiation is excessively deep and possibly infinite”.

Tuple length is the standard trick for arithmetic: build a tuple of N elements, then read ["length"].

Variance: when is a container of Dogs a container of Animals?

Variance is the question of how a relationship between two types (Dog extends Animal) carries over to types built from them (Dog[] and Animal[]). It has four possible answers, and the terms are worth having:

TypeScript knowingly gets one case wrong for convenience: arrays are treated as covariant even though they are writable. Assign a Dog[] to an Animal[], push an Animal, and the original array now holds something that is not a Dog, with no error anywhere. File 40 shows the two ways to close it.

The related wrinkle is that the same declaration is checked strictly as a property and loosely as a method — method syntax is bivariant. The loose version exists because the standard library requires it. Prefer the property form in your own code.

You can also write the direction down: in, out, and in out variance annotations. They do not change behaviour — they document intent, catch you if a later member contradicts them, and speed up checking.

this types

this as a type means “whatever the receiver turns out to be”, which is what keeps a fluent chain working after someone subclasses you — file 04’s builder, seen from the type side. ThisType<T> sets the this inside an object literal’s methods, which is how configuration-object APIs get typed.

The patterns worth knowing by name

None of these are language features. Each is the rules above, composed — and knowing the names saves reverse-engineering them at 2am.

Where this leaves you

You now have both halves of TypeScript: a description layer and a computation layer over it. What you do not have is any account of where types come from across file boundaries, or what happens at the places the type layer touches something it cannot see.

That is file 10, and it is the only file in the census that does not simply run. Everything in files 01–09 is either plain JavaScript or type syntax that is erased. File 10 collects the constructs that emit codeenum, namespace, parameter properties, decorators — plus the declaration syntax that describes code the checker will never look at. Both halves of that are the same subject: the seam between a checked world and an unchecked one, and every unsoundness in the language lives on it.

While you read

The file

Read one construct at a time and evaluate it by hand for a small input before reading the comment. Mark anything you cannot explain with // ? in the source.

// 09 — CONDITIONAL, MAPPED, AND TEMPLATE LITERAL TYPES; VARIANCE
//
// This is where TypeScript becomes a language of its own — pure, lazy,
// functional. Conditional types are `if`, mapped types are a loop, `infer` is
// pattern matching, and template literals do string work. Once you see that, the
// syntax explains itself.
//
// A reading file. Two markers, since almost everything here is a type:
//
//   // type: X   what the checker resolves this to, in its own words
//   // =>  v     what the expression evaluates to, on the rare line that runs
//
// Section 10 re-checks the interesting ones with a compile-time assertion, so
// the claims in this file are enforced by the checker rather than by me.
//
// It still runs (`node 09-types-advanced.ts`) and prints nothing.
//
// Mark anything you can't explain out loud with  // ?

// ─── 1. THE TYPE-LEVEL OPERATORS ─────────────────────────────────────────────
//
// Three operators to move between the value world and the type world, and one to
// look inside a type. Everything later is built from these.

const value = { a: 1, b: "s", nested: { c: true } };

type TypeOf = typeof value; // VALUE WORLD -> TYPE WORLD
// type: { a: number; b: string; nested: { c: boolean; }; }
type Keys = keyof TypeOf; // type: "a" | "b" | "nested" — the KEYS as a union of literals
type Indexed = TypeOf["nested"]["c"]; // type: boolean — INDEXED ACCESS, chainable
type ArrayElement = (string[])[number]; // type: string — [number] gets the element type
type TupleElements = [1, 2, 3][number]; // type: 1 | 2 | 3 — every position, as a union
type ValueUnion = TypeOf[keyof TypeOf]; // type: number | string | { c: boolean; }
// — index by every key at once and you get every value type at once

type KeyofRecord = keyof Record<string, number>; // type: string
type KeyofIndexSignature = keyof { [k: string]: number }; // type: string | number
// Those two describe the same object and disagree. `Record` is a MAPPED type, and
// keyof a mapped type is exactly the union it was mapped over. A hand-written
// INDEX SIGNATURE also admits numeric keys, because `obj[0]` and `obj["0"]` are
// the same property — so `number` comes back too.
type KeyofArray = keyof string[]; // type: number | "length" | "at" | "pop" | ... — every
// method name as well, because those are properties like any other

// ─── 2. CONDITIONAL TYPES ────────────────────────────────────────────────────
//
// `A extends B ? X : Y` is an if. But read `extends` as "is assignable to", not
// "is exactly" — and then watch what happens when you hand it a union.

type IsString<T> = T extends string ? true : false;
type A1 = IsString<"a">; // type: true
type A2 = IsString<1>; // type: false

// This is the behaviour to really understand, because it is invisible until it
// bites. When the checked side is a BARE type parameter and the argument is a
// union, the conditional runs once per member and the answers are unioned back
// together. Not once for the whole union — once each.
type Distributed<T> = T extends string ? "str" : "other";
type D1 = Distributed<string | number>; // type: "str" | "other"

// And this is how you turn distribution OFF: wrap either side in a tuple, and the
// union is tested as a single thing. Worth being able to do deliberately.
type NotDistributed<T> = [T] extends [string] ? "str" : "other";
type D2 = NotDistributed<string | number>; // type: "other"

// `never` is the union with NO members, so distributing over it runs zero times
// and produces nothing. Neither branch is evaluated at all (39.2).
type D3 = Distributed<never>; // type: never — NOT "other"
type D4 = NotDistributed<never>; // type: "str" — [never] does extend [string]

// One more that catches people: `boolean` is a union too, so it splits.
type D6 = boolean extends true ? "yes" : "no"; // type: "no" — boolean is not `true`
type D7<T> = T extends true ? "t" : "f";
type D8 = D7<boolean>; // type: "t" | "f" — distribution over `true` and `false`

// keyof flips the operation, which sounds wrong until you say it out loud: given
// "an A or a B", the only keys you can safely read are the ones BOTH have.
type K1 = keyof ({ a: 1; shared: 2 } | { b: 3; shared: 4 }); // type: "shared"
type K2 = keyof ({ a: 1 } & { b: 2 }); // type: "a" | "b"

// ─── 3. infer ────────────────────────────────────────────────────────────────
//
// `infer U` is pattern matching: describe a shape, and name the part you want
// pulled out of it. It only works in the `extends` clause, because that is the
// only place a match is happening.

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UnwrapArray<T> = T extends (infer U)[] ? U : never;
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FirstIfString<T> = T extends [infer F extends string, ...unknown[]] ? F : never;
// ...that last one is `infer` with a CONSTRAINT (TS 4.7+)

type B1 = UnwrapPromise<Promise<string>>; // type: string
type B4 = MyReturnType<() => number>; // type: number
type B5 = MyParameters<(a: string, b: number) => void>; // type: [a: string, b: number]
type B6 = FirstIfString<["a", 1]>; // type: "a"

// The same name matched in several places COMBINES — and how it combines depends
// on where. Values that could come out of either place: union. Parameters that
// must accept whatever arrives: intersection. That asymmetry is a tool, not a
// quirk (39.5).
type ToUnion<T> = T extends { a: infer U; b: infer U } ? U : never;
type ToIntersection<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;

type B2 = ToUnion<{ a: 1; b: 2 }>; // type: 1 | 2 — covariant positions union
type B3 = ToIntersection<{ a: (x: { p: 1 }) => void; b: (x: { q: 2 }) => void }>;
// type: { p: 1; } & { q: 2; } — contravariant positions intersect

// ─── 4. MAPPED TYPES ─────────────────────────────────────────────────────────
//
// A loop over keys. The body of the type IS the loop; there is no other member.
// `+` and `-` add or remove `readonly` and `?` as you go.

interface Source { a: string; b?: number; readonly c: boolean }

type Identity<T> = { [K in keyof T]: T[K] }; // the base form
type AllOptional<T> = { [K in keyof T]?: T[K] };
type AllReadonly<T> = { readonly [K in keyof T]: T[K] };

type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // `-` REMOVES a modifier
type Concrete<T> = { [K in keyof T]-?: T[K] }; // and `-?` also strips `| undefined`

type Flags = { [K in "read" | "write"]: boolean }; // mapping a union of keys directly
// type: { read: boolean; write: boolean; }

// `as` renames each key. And mapping a key to `never` DELETES it, which is the
// only way to filter — worth recognising on sight, since it looks nothing like
// deletion.
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };
//                                        ^ a key mapped to `never` disappears

type C1 = Getters<{ name: string }>; // type: { getName: () => string; }
type C2 = OnlyStrings<Source>; // type: { a: string; }
type C4 = Concrete<Source>; // type: { a: string; b: number; readonly c: boolean; }

// Write it in exactly the shape `{ [K in keyof T]: ... }` and you get a special
// case: modifiers are preserved automatically, and mapping an ARRAY gives back an
// array rather than an object with `push` as a property. That is what makes
// Partial and Readonly usable on collections at all (39.4).
type MappedArray = AllOptional<string[]>; // type: (string | undefined)[] — still an array
type MappedTuple = Mutable<readonly [1, 2]>; // type: [1, 2] — still a tuple

const remapped: C1 = { getName: () => "n" };
const throughGetter = remapped.getName(); // => "n"

// ─── 5. TEMPLATE LITERAL TYPES ───────────────────────────────────────────────
//
// String manipulation at the type level. A union in one slot multiplies out
// against every other slot, so these grow fast — the checker gives up at 100,000
// members.

type EventName<T extends string> = `on${Capitalize<T>}`;
type CssUnit = `${number}px` | `${number}%`;
type Route = `/${string}`;

type Cases = [Uppercase<"a">, Lowercase<"A">, Capitalize<"ab">, Uncapitalize<"Ab">];
// type: ["A", "a", "Ab", "ab"] — the four intrinsic string manipulators
type Corner = `${"top" | "bottom"}-${"left" | "right"}`;
// type: "top-left" | "top-right" | "bottom-left" | "bottom-right" — two unions of two
type Clicked = EventName<"click">; // type: "onClick"

// Combine templates with `infer` and you can take strings apart. This is how
// typed route parameters and typed object paths are built.
type Split<S extends string, D extends string> = S extends `${infer Head}${D}${infer Tail}`
  ? [Head, ...Split<Tail, D>]
  : [S];
type PathParams<S extends string> = S extends `${string}:${infer Param}/${infer Rest}`
  ? Param | PathParams<Rest>
  : S extends `${string}:${infer Last}`
    ? Last
    : never;

type E1 = Split<"a.b.c", ".">; // type: ["a", "b", "c"]
type E2 = PathParams<"/users/:id/posts/:postId">; // type: "id" | "postId"

const corner: Corner = "top-left";
const unit: CssUnit = "10px";
// `const bad: CssUnit = "10em"` is TS2322 — the pattern really is checked.

// ─── 6. RECURSIVE TYPES ──────────────────────────────────────────────────────
//
// Types can refer to themselves as long as the reference sits inside something —
// an object, an array, a tuple. A bare `type A = A` has no shape to work with and
// is rejected.

type Json = string | number | boolean | null | Json[] | { [key: string]: Json };

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

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

// Where the recursive call sits matters. If it is the whole result — TAIL
// POSITION — you get about 1000 levels. Otherwise about 50. Same logic, twenty
// times the room. Exceeding either is TS2589, "Type instantiation is excessively
// deep and possibly infinite".
type BuildTuple<N extends number, Acc extends unknown[] = []> = Acc["length"] extends N
  ? Acc
  : BuildTuple<N, [...Acc, unknown]>;
type Add<A extends number, B extends number> = [...BuildTuple<A>, ...BuildTuple<B>]["length"];

type F1 = Add<2, 3>; // type: 5 — arithmetic, done entirely with tuple lengths
type F2 = DeepReadonly<{ a: { b: number } }>; // type: { readonly a: { readonly b: number; }; }

const json: Json = { a: [1, "s", null] };
const nestedJson = (json as { a: Json[] }).a[1]; // => "s"

// ─── 7. VARIANCE ─────────────────────────────────────────────────────────────
//
// The question here: if a Dog is an Animal, when is a container of Dogs a
// container of Animals? The answer depends on whether you can WRITE to it — and
// TypeScript knowingly gets one case wrong, for convenience.

class Animal { move() {} }
class Dog extends Animal { bark() {} }

// Here is the unsound one, and it is worth going slowly. Reading from an Animal[]
// that is really a Dog[] is fine. WRITING to it is not, and arrays are writable.
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // allowed — arrays are treated as COVARIANT
animals.push(new Animal()); // ...and `dogs` now holds something that is not a Dog
const dogCount = dogs.length; // => 2 — the same array, seen through two types
const lastIsADog = dogs[1] instanceof Dog; // => false — with no error anywhere.
// See 40.5 for the two ways to close this.

// Function parameters go the other way, and that direction is the safe one: a
// handler that copes with any Animal can obviously stand in for a Dog handler.
type Handler<T> = (value: T) => void;
const animalHandler: Handler<Animal> = (a) => a.move();
const dogHandler: Handler<Dog> = animalHandler; // CONTRAVARIANT, and sound
// The reverse assignment is TS2322: a Dog handler cannot take just any Animal.

// A wrinkle worth knowing: the SAME declaration is checked strictly as a property
// and loosely as a method. The loose version exists because the standard library
// needs it — Array<Dog> has to be an Array<Animal>. Prefer the property form in
// your own code (37.2).
interface MethodStyle { handle(v: Dog): void } // BIVARIANT — accepts either handler
interface PropertyStyle { handle: (v: Dog) => void } // strictly checked

// You can write the direction down. It changes no behaviour: it documents intent,
// catches you if a later member contradicts it, and speeds up checking.
interface Producer<out T> { get(): T } // COVARIANT — T only comes out
interface Consumer<in T> { set(v: T): void } // CONTRAVARIANT — T only goes in
interface Both<in out T> { get(): T; set(v: T): void } // INVARIANT — both, so neither

const producer: Producer<Animal> = { get: () => new Dog() } as Producer<Dog>;
const producedIsAnimal = producer.get() instanceof Animal; // => true — safe, because a
// Producer only ever hands values out

// ─── 8. this TYPES AND ThisType ──────────────────────────────────────────────
//
// `this` as a TYPE means "whatever the receiver actually turns out to be", which
// is what keeps a fluent chain working after someone subclasses you.

interface Chainable {
  step(): this; // POLYMORPHIC this — the subclass's own type flows through
}

// ThisType<T> sets what `this` means inside an object literal's methods. It is
// how a config object gets a `this` it never declared.
type Store = { count: number } & ThisType<{ count: number; increment(): void }>;
const store: Store & { increment(): void } = {
  count: 0,
  increment() {
    this.count++; // `this` is typed by ThisType, not by the literal it sits in
  },
};
store.increment();
const counted = store.count; // => 1

// ─── 9. THE PATTERNS WORTH KNOWING BY NAME ───────────────────────────────────
//
// Five things you will meet in other people's type code. None are language
// features — each is the rules above, composed. Knowing the names saves you
// reverse-engineering them at 2am.

// BRANDING. TypeScript matches by shape, so a UserId and a PostId are the same
// type. Adding a property nothing else can have makes them genuinely distinct, at
// zero runtime cost, since the value is still just a string.
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;

const userId = "u1" as UserId;
const brandIsErased = typeof userId; // => "string" — nothing was added to the value
// `const wrong: PostId = userId` is TS2322: structurally identical, nominally distinct.

// PRETTIFY does nothing whatsoever except force the checker to expand a type, so
// tooltips show `{ a: 1; b: 2 }` instead of `A & B`. Pure ergonomics.
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type G1 = Prettify<{ a: 1 } & { b: 2 }>; // type: { a: 1; b: 2; }

// UNIONTOINTERSECTION — the famous one, and it is only section 2 and section 3
// stacked: distribute a union into function parameter position, then infer that
// parameter back out, which intersects.
type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends
  (x: infer I) => void ? I : never;
type G2 = UnionToIntersection<{ a: 1 } | { b: 2 }>; // type: { a: 1; } & { b: 2; }

// EQUALS. Assignability is not equality — `any` is assignable both ways to
// everything. Wrapping both sides in a deferred conditional forces a real
// identity comparison instead.
type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
  ? true
  : false;
type Assert<T extends true> = T;

// ─── 10. THE CLAIMS ABOVE, CHECKED BY THE COMPILER ───────────────────────────
//
// Every line below fails to compile if the corresponding `// type:` comment
// earlier in the file is wrong. This is the file checking itself: `npm run check`
// is the verification, not a promise in a comment.

type Check1 = Assert<Equals<A1, true>>;
type Check2 = Assert<Equals<D1, "str" | "other">>;
type Check3 = Assert<Equals<D2, "other">>;
type Check4 = Assert<Equals<D3, never>>;
type Check5 = Assert<Equals<D4, "str">>;
type Check6 = Assert<Equals<D8, "t" | "f">>;
type Check7 = Assert<Equals<K1, "shared">>;
type Check8 = Assert<Equals<K2, "a" | "b">>;
type Check9 = Assert<Equals<B2, 1 | 2>>;
type Check10 = Assert<Equals<B5, [a: string, b: number]>>;
type Check11 = Assert<Equals<B6, "a">>;
type Check12 = Assert<Equals<C2, { a: string }>>;
type Check13 = Assert<Equals<MappedTuple, [1, 2]>>;
type Check14 = Assert<Equals<E1, ["a", "b", "c"]>>;
type Check15 = Assert<Equals<E2, "id" | "postId">>;
type Check16 = Assert<Equals<Clicked, "onClick">>;
type Check17 = Assert<Equals<Add<2, 3>, 5>>;
type Check18 = Assert<Equals<Keys, "a" | "b" | "nested">>;
type Check19 = Assert<Equals<TupleElements, 1 | 2 | 3>>;
type Check20 = Assert<Equals<Cases, ["A", "a", "Ab", "ab"]>>;
type Check21 = Assert<Equals<ValueUnion, number | string | { c: boolean }>>;
type Check22 = Assert<Equals<KeyofRecord, string>>;
type Check23 = Assert<Equals<KeyofIndexSignature, string | number>>;
type Check24 = Assert<"length" extends KeyofArray ? true : false>;
type Check25 = Assert<number extends KeyofArray ? true : false>;
type Check26 = Assert<Equals<C1, { getName: () => string }>>;
type Check27 = Assert<Equals<C4, { a: string; b: number; readonly c: boolean }>>;
type Check28 = Assert<Equals<Flags, { read: boolean; write: boolean }>>;
type Check29 = Assert<
  Equals<Corner, "top-left" | "top-right" | "bottom-left" | "bottom-right">
>;
type Check30 = Assert<Equals<G1, { a: 1; b: 2 }>>;
type Check31 = Assert<Equals<MappedArray, (string | undefined)[]>>;
type Check32 = Assert<Equals<F2, { readonly a: { readonly b: number } }>>;
// `type Check33 = Assert<Equals<any, unknown>>` fails, which is the point of Equals.