Advanced TypeScript types
Conditional types and the meaning of extends
A conditional type is T extends U ? X : Y: you get the true branch when T is assignable to U, the false branch otherwise. extends here means "assignable to," the same structural rule the compiler applies everywhere else, not class inheritance.
Wrong "extends in a conditional checks class inheritance." A plain object literal with the right shape passes it; a class that only shares a name does not.
The true branch also narrows the checked type, so once a shape is proven you can index into it:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
// MessageOf<{ message: string }> -> string ; MessageOf<Dog> -> never
Distributive conditionals, recapped
You built Exclude and Extract on distribution in ts-generics, so here it is compressed: a naked type parameter (T alone on the left of extends) that receives a union runs the conditional once per member and unions the results, which is why ToArray<string | number> is string[] | number[] and not (string | number)[]. Two bites carry into this module. never is the empty union, so a naked conditional over it runs zero times and collapses to never (ToArray<never> is never, not never[]). And boolean is true | false, so a naked conditional over it quietly runs twice. Wrap both sides in a one-tuple, [T] extends [U], to judge the union whole and turn distribution off.
Capturing types with infer
infer declares a fresh type variable inside the extends pattern, bound to whatever sits in that position and usable only in the true branch. It is the machinery behind ReturnType and Parameters that you already have from ts-generics.
type Flatten<T> = T extends Array<infer Item> ? Item : T;
The wrinkle this module adds is what happens when the same infer variable appears more than once. In covariant positions (return types) the compiler unions the candidates; in contravariant positions (parameters) it intersects them:
type Ret<F> = F extends { a: () => infer U; b: () => infer U } ? U : never;
// Ret<{ a: () => string; b: () => number }> -> string | number
type Arg<F> = F extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
// Arg<{ a: (x: A) => void; b: (x: B) => void }> -> A & B
That is the same covariant-return, contravariant-parameter variance rule the senior note works through, showing up inside inference.
Mapped type modifiers
A mapping modifier is a + or - prefix on readonly or ?, written right before the colon. The default prefix is +, so a bare ? adds optionality, while -? and -readonly strip them. This is exactly how Required and the mutable variants are built:
type Concrete<T> = { [K in keyof T]-?: T[K] };
Key remapping with an as clause (renaming a key, or dropping it by mapping to never) and the Capitalize<string & K> template-literal trick are ts-generics ground already. What this module leans on is the modifier syntax above plus the template-literal machinery covered in the senior note.
Template literal types and combinatorial blowup
Template literal types build string-literal types with the JS template syntax in type position. Given type World = "world", the type `hello ${World}` resolves to "hello world". Interpolating a union is where scale bites, because each slot cross-multiplies. `${"en" | "ja"}_${"title" | "body"}` expands to the four-member product, one string per combination, and a third union multiplies again. Type-level string DSLs hit compiler recursion limits and produce errors no one can read.
infer works inside the pattern, so you can pull a string apart: T extends `${infer Head}.${infer Tail}` ? Head : T splits on the first dot. The four intrinsic types Uppercase, Lowercase, Capitalize, and Uncapitalize change casing by calling the JavaScript runtime string functions, so they are not locale-aware.
Covariance, contravariance, and invariance
Variance is how a wrapper's assignability tracks its type argument. Getter<T> = () => T is covariant: Getter<Dog> is assignable to Getter<Animal>, the same direction as Dog to Animal, because a function producing a Dog serves anywhere one producing an Animal is expected. Setter<T> = (value: T) => void is contravariant: the direction flips, so Setter<Animal> is assignable to Setter<Dog>. A parameter used in both positions, as in interface State<T> { get: () => T; set: (value: T) => void }, is invariant, assignable in neither direction. The arrow-property syntax matters here: write set as a method, set(value: T): void, and its parameter stays bivariant by the exemption below, so State would measure covariant instead of invariant.
TS 4.7 added optional annotations: out for covariant, in for contravariant, in out for invariant, as in type Getter<out T> = () => T. They are structurally checked, not hints. Mark State<out T> while T still appears in set and the compiler errors that the annotation contradicts actual usage.
Parameter bivariance and strictFunctionTypes
By default TypeScript checks function parameter positions bivariantly: (x: Dog) => void and (x: Animal) => void are mutually assignable, which is unsound but was kept for event-handler ergonomics. Under strictFunctionTypes (on with strict) parameters are checked contravariantly. With f1: (x: Animal) => void and f2: (x: Dog) => void, f2 = f1 type-checks and f1 = f2 errors, since a Dog-only handler cannot cover every case where an Animal is passed.
Wrong "strict mode makes every parameter contravariant." Methods and constructor declarations are exempt and stay bivariant. compare(a: T, b: T): number in method syntax is bivariant; the same signature as a property, compare: (a: T, b: T) => number, is contravariant. The exemption keeps Array<T> and Promise<T> relating covariantly.
Why arrays are unsoundly covariant
Arrays inherit that exemption. Array<T>'s mutators are declared as methods, so T[] stays covariant even under strict, and Cat[] is assignable to Animal[]. That is unsound the moment you mutate: take the Animal[] view, push(new Dog()), and the backing Cat[] now holds a non-cat, so a later cat.meow() compiles and throws at runtime. Sound typing would make mutable arrays invariant; TypeScript accepts the hole for ergonomics. A readonly T[] has no push, so its covariance is sound, and it is the annotation to reach for on read-only inputs.
Function parameters default to bivariant, method and constructor parameters stay bivariant even under strictFunctionTypes, and that method exemption is exactly what leaves mutable arrays unsound.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.