TypeScript Type System
TypeScript
Foundations
- three declaration spaces — value (const/let/var/function/params), type (interface/type/type params), namespace (dotted access A.B.C) — one name can occupy more than one space at once, e.g. a class is both value and type
- structural typing — TS compares shapes, not declared names — but two same-shape classes with private members are NOT interchangeable
- type erasure — types exist compile-time only; instanceof works against a class (real JS value) but errors (TS2693) against an interface
- keyof T — union of T's key names; keyof(A|B) = shared keys only, keyof(A&B) = all keys
- generic constraints <T extends U> — bounds what T must satisfy — a method spec { length(): number } differs from a property spec length: number
- generic defaults <T = X> — a required type parameter may never follow a defaulted one
- <K extends keyof T> — typed key parameter — the derived value T[K] goes in the value slot, not the header
- indexed access T['k'] / T[number] — pulls a property's/element's type out; a union key T['a'|'b'] gives the union of those value types
- typeof type query — value -> type, e.g. ReturnType<typeof fn> — distinct from runtime typeof narrowing below
- generic class Box<T> — the call site is the spec; constructors and methods must honor the T it fixes
- enum vs literal union — no = ever follows the enum name (only members use =); a literal union usually beats a numeric enum
- typeof narrowing (values) — runtime typeof checks narrow a union at the value level — distinct from the type-space typeof query above; typeof returns one of: "string", "number", "boolean", "undefined", "object", "function", "symbol", "bigint"
- string -> number conversion — canonical is Number(x); parseInt/parseFloat parse only a leading prefix, ignoring trailing garbage
- checking if a string is numeric — !isNaN(Number(x)) — global isNaN(x) alone coerces first, so isNaN("") is false and isNaN("abc") is true
- generics <T> on a function — keeps a signature value-agnostic instead of hardcoding one type
- union types A | B — the value could be either — narrow before using members not common to both
- assignability direction — narrower assigns to wider, never the reverse — string | undefined is not assignable to string
- intersection A & B — combines members; a property with conflicting types across A and B collapses to never
- interface vs type — interface X = {...} is illegal (interfaces never use =); interfaces merge by declaration, type aliases never do
- literal types — a single exact value as a type; let widens to the general type (string), not assignable back to the literal
- union of literals — 'a' | 'b' | 'c' — keep every member with |
- rest vs spread — same ..., opposite directions — a new name next to ... is gathering (rest); an existing value next to ... is spreading
Type erasure
- which declarations emit JS — const / class / enum / namespace emit real runtime code; type, interface, and pure type syntax erase completely
- instanceof needs a value — works against a class (real constructor); errors TS2693 against an interface (no runtime representation)
- what a runtime check CAN interrogate — typeof / instanceof / in can only test real JS values and shapes, never a type
- a generic type parameter at runtime — T itself is not a value — new T() and instanceof T both fail; pass a real constructor in instead
- enum emits a real object — numeric enums map both directions (name<->value); string enums map name->value only
- discriminated union tag is a real property — no type exists at runtime, so a switch can only branch on an actual property, never a type
- namespace emits a value — a namespace compiles to a real runtime object
- private/protected/readonly vs # — private/protected/readonly are compile-time only (still a readable JS property); # is the one real runtime-private mechanism
- as and ! perform no check — both are purely compile-time — neither runs code nor verifies anything at runtime
- declare and .d.ts describe, never create — referencing an absent declared value throws ReferenceError, not a silent undefined
- emitDecoratorMetadata — the deliberate partial de-erasure that lets NestJS-style DI resolve classes (never interfaces) as tokens at runtime
Nominal behavior
- instanceof behaves nominally — walks the real prototype chain (actual constructor identity) — a structurally-identical plain object fails it even though it satisfies the class's shape
- branded / nominal types — string & {__brand} or unique symbol — not assignable from the plain type; a brand does not survive serialization
- private/protected members — a class with a private/protected member is only assignable from that exact declaration — the one exception structural typing carves out for classes
- string enums — TS's one genuinely nominal built-in type — a plain string literal like 'active' is not assignable to the enum type
any / unknown / never
- unknown (safe top type) — accepts anything in, blocks all use until narrowed — even typeof === 'object' has a null hole
- any (escape hatch, also a top type) — unchecked in both directions; unknown is the safer default — narrow before use
- never (bottom type) — the type of a value that can't exist; T | never = T, T & never = never
- exhaustiveness check (const _: never = x) — assigning the leftover branch to a never-typed variable fails to compile if a union case was missed
- {} — the non-nullish top type — accepts everything except null/undefined; object rejects primitives
Narrowing & discriminated unions
- tagged union + narrow on the tag — switch/if on the shared literal tag; only members common to every branch are readable before narrowing
- narrowing via switch + exhaustiveness — falling off the end yields undefined; the error only appears if the declared return type rejects it
- instanceof / in as guards — 'key' in x narrows by property presence — no tag needed
- checking if a value is an array — Array.isArray(x) — typeof x on an array is just "object", it can't tell arrays apart
- user-defined guard x is T — a function returning x is T; narrows the caller's variable after a truthy check — the key in x is T must be quoted
- type guard inside .filter — TS 5.5+ infers the predicate automatically — no x is T annotation needed
- narrow unknown in catch — catch bindings are unknown under strict mode; a throw can deliver a non-Error, so guard before use
Assertions
- as type assertion — legal only either-direction between the two types; compiles but lies at runtime — a as Dog still has no real .breed
- as const — deep readonly at every depth; value-space only — illegal on the right of type X = ...
- non-null assertion x! — emits nothing at runtime; a wrong ! just fails later at the property read
- satisfies operator — checks the value against a type but keeps the narrower inferred type — no widening
- double assertion as unknown as T — bypasses the overlap check entirely; nothing is checked or converted at runtime
Utility types
- Partial<T> / Required<T> — make every property optional / make every property required
- Pick<T,K> / Omit<T,K> — select or drop named keys — usable as a return type, not just inline
- Record<K,V> — a literal-union key makes every key required; a plain string key stays open (partial)
- Readonly<T> / readonly T[] / tuples — shallow only — one level deep, not recursive
- ReturnType<T> — extracts a function type's return type; needs typeof on a value first to get its type
- Parameters<T> — extracts a function type's parameter list as a tuple
- Awaited<T> — recursively unwraps nested Promises
- Exclude / Extract / NonNullable — Exclude removes matching union members, Extract keeps only matching ones, NonNullable drops null/undefined
Mapped types
- { [K in keyof T]: ... } — rebuilds an object type by iterating its keys; the homomorphic form passes modifiers through unchanged
- modifiers ? / readonly / -? / -readonly — add or strip optional/readonly per key
- key remapping as in mapped types — { [K in keyof T as NewKey]: ... } renames keys, or filters one out entirely when the as clause resolves to never
Conditional types
- T extends U ? X : Y — a type-level if/else based on assignability
- distributive over unions — a naked type parameter in the check position distributes over each union member; over never it collapses to never
- distribution needs a NAKED type parameter — wrapping it in a tuple, [T] extends [U], turns distribution off
- recursive conditional & mapped types — a conditional/mapped type can recurse to transform nested structures, e.g. DeepReadonly<T>
- infer — placement & multiple sites — introduces a type variable inside the extends clause to capture part of the matched type
- infer (extract a type) — the standard way to pull one piece — an element, a return type — out of a larger type
Template-literal types
- `${A}-${B}` template-literal types — build string literal types like template strings; distributes over unions in each slot (cross product)
- Uppercase / Lowercase / Capitalize / Uncapitalize — the built-in intrinsic type-level string transforms
Function types
- function overloads — resolution — first matching signature (declaration order) wins; return types never merge or union
- call signature — the braces form — type F = { (x: string): number } — a callable value type; add named members for a callable object
- construct signature — new (...args) => T types a constructor itself; abstract new (...) => T types an abstract class
- the match-all function type — (...args: never[]) => unknown accepts any function; the unknown[] version rejects typed-parameter functions by contravariance
- type parameter position with a callback — a shared <T> can sit outside, in the callback's return, or in the callback's own parameter — the parameter slot is easiest to miss
- generic wrapper around a function — where the hole goes — put <T> on the piece that should vary, not the whole shape — <T extends (...args: any[]) => any> only proves 'is a function'
- inference through a contravariant callback parameter — candidates from a callback parameter intersect, not union — string and number there infers T = string & number
- overloads collapsing into one generic — a verified exception — a rest-parameter generic over a union of tuple shapes, <T extends [Date,Date]|[number]>(...args: T), can legally replace overloads that looked irreducible
- composing functions — typing pipe/compose — a generic pipe/compose signature threads one type variable through every stage
Classes
- readonly / param properties — readonly blocks reassignment at compile time only — it never freezes the value at runtime
- public / private / # — private/protected are compile-time only and still ordinary readable JS properties; # is genuinely private at runtime
- implements — a pure compile-time contract check — zero runtime effect, and a mismatch errors at the class name
- method vs arrow-function field — m() {} binds this at the call site (detachable, can lose it); m = () => {} binds this once per instance (safe as a passed-around callback, costs one field per instance) — prefer the arrow field for event handlers
- get/set accessors — get x(): T / set x(v: U) — a getter with no setter is externally readonly; TS 4.3+ allows the setter's parameter type to differ from the getter's return type; the pair counts as one member for an interface
Runtime validation
- excess-property check (literal vs parsed) — only fires on a fresh object literal assigned directly — a variable or already-parsed value skips it
- parse, don't validate — return the narrowed value itself from a parser, not just a boolean — a guard alone strips/transforms nothing
- derive the type from the schema — type X = z.infer<typeof schema> — infer needs typeof on the schema value, and it's a generic <>, never a call ()
- throw vs result (parse / safeParse) — .parse throws on failure; .safeParse returns a discriminated Result instead
- unknown-key policy — a schema library must decide: strip, reject, or keep keys it didn't declare
- TS has no exact object type — no extra keys isn't expressible structurally — extras are always assignable except the excess-property check on a fresh literal
- Checked<T> — a wrapper type only producible by actually running validation, so an unvalidated value can't type-check as trusted
- never spread into persistence — map fields explicitly rather than spreading an object into a DB write, so extra/renamed fields can't sneak through
- boundaries — where untyped data enters — HTTP request bodies; third-party API responses; queue/workflow inputs; event/pub-sub messages; file uploads; config/env vars; database reads
- where validation lives — parse once at the edge; everything downstream trusts the typed value
- coercion: strings in, typed values out — schema coercion turns raw string input into typed output — input and output types can legitimately differ
- schema composition mirrors utility types — zod's .omit takes a mask object, not a string, the same shape as the Omit<T,K> utility type
Variance & assignability
- arrays are covariant (and unsound) — Dog[] assigns to Animal[], which lets an unsound push through the wider reference — the fix is readonly T[]
- function parameters are contravariant — a function accepting a WIDER parameter type is assignable where a narrower one is expected
- method params are bivariant (the deliberate hole) — method-shorthand syntax checks parameters loosely both ways (unsound, kept for practicality); property-syntax stays strictly contravariant
- return types are covariant — a function returning a NARROWER type is assignable where a wider return is expected
- ReadonlyArray<T> vs T[] — a one-way door — T[] assigns into ReadonlyArray<T> freely; the reverse is always rejected
- tuples and variance — a tuple assigns into a compatible array type; the reverse (array into tuple) is rejected
- Map<K,V> covariant in V — because its lib method signatures use shorthand (bivariant) syntax, not strict variance
- in / out variance annotations — declare a generic's variance explicitly and let the compiler check it — violation is TS2636
- keyof T is contravariant in T — a wider T produces a narrower keyof T (fewer guaranteed keys) — the direction inverts
- mutable object property covariance — the same unsound hole as arrays — a narrower-typed object aliased through a wider reference lets you write back an incompatible value
- your own generic — measure where T sits — where T appears (return-only / parameter-only / both / unused) decides covariant, contravariant, invariant, or bivariant
- the phantom type — an unused T is bivariant — a type parameter that never appears in any member behaves bivariantly, since nothing structurally constrains it
- void return accepts any return type — a callback typed to return void still accepts a function that returns something — the value is just ignored
- parameter count — a function needing FEWER parameters than expected is assignable; needing MORE is not
- tuple arity vs element variance — length compatibility and per-element variance are checked as separate, independent rules
- optional is presence, not variance — an optional property changes whether the key must exist, not the compatibility rules for its value type
- readonly on a property is not checked — invisible to assignability for plain properties — only readonly arrays get a dedicated check, TS4104
- never and unknown as the two edges — any is assignable to everything except never; unknown accepts everything and is assignable to almost nothing
- T[K], index signatures, construct signatures — variance applies at each position independently — an index-signature read can be unsound without noUncheckedIndexedAccess
- higher-kinded types and the workarounds — a bare type constructor F<_> as a parameter isn't legal TS (TS2315); libraries like fp-ts encode it indirectly via a Kind pattern
Inference
- widening: let vs const on a literal — let widens a literal to its general type; const keeps the literal type
- why an object literal's property widened — a plain property inside an object literal widens the same way let would, unless fixed by as const, an explicit type, or context
- when a union widens (and when it collapses) — let widens every member of an inferred union; TS also collapses redundant subtypes into a wider member
- contextual typing — inference flows from the expected type at a position (e.g. a callback parameter's type comes from where it's passed)
- inference from a generic call site — TS infers one T per call by picking a best-fit candidate — it does not union multiple candidates together
- generic vs overload — the decision — collapse overloads into one generic only when the return type doesn't change shape between forms
- groupBy that preserves key literals — the key type should be inferred from the callback's return (K extends string), not from keyof T
- NoInfer<T> — marks a position that participates in the check but is excluded from what fills the type parameter
- declaration merging — same-named interfaces merge their members; type aliases never merge
- constraint vs inference — two different jobs — a constraint (extends) only gates which arguments are legal — it does not narrow what gets inferred for T
- when a constraint DOES change what's inferred — a primitive constraint like T extends string turns off literal widening for that argument, but not for arrays
- keep literals & tuples at a call site — three ways: as const at the call site, a <const T> type parameter, or an explicit narrow annotation
- life of a type parameter — fill order — explicit <> first, then arguments, then the expected result type, then a default, then unknown last
Library-grade authoring
- derive union from array (typeof ARR[number]) — turns a const array's element type into a union without repeating the literals
- build a small utility type from scratch — hand-roll a Pick/Omit-shaped type to prove the mapped/conditional mechanics, not just recognize the built-in
- variadic tuples [...T] and [H, ...infer R] — spread/destructure tuple types like array patterns — infer stays lowercase, a dropped slot still needs a real type, not a made-up name
- assertion functions: asserts x is T — narrows its argument by throwing instead of returning a boolean — only narrows when called through an explicitly-typed reference (TS2775)
- const type parameters <const T> — infers the literal/tuple type as given instead of widening it
- abstract new (...) => T — types an abstract class itself, not just its instances — typeof SomeClass is a class name used as a value/parameter type
Vocabulary
- assignability — the core question every rule answers: can a value of type A be used where type B is expected?
- widening — a literal/narrow type relaxing to its general type once nothing anchors it narrow
- narrowing — shrinking a union to a smaller set of members via a runtime check
- variance — the umbrella term for how subtyping of T affects subtyping of a type built from T, e.g. Box<T>
- covariant — subtyping direction is preserved: Dog <: Animal implies Box<Dog> <: Box<Animal>
- contravariant — subtyping direction flips: a function accepting Animal is a subtype of one accepting Dog
- invariant — neither direction holds — Box<Dog> and Box<Animal> are unrelated unless T matches exactly
- bivariant — accepted both ways unsoundly — TS's deliberate hole for method-shorthand parameters
- distributive — a conditional type that, given a naked union type parameter, applies itself to each member separately
- homomorphic — a mapped type that iterates keyof T and structurally mirrors T's own modifiers (readonly/optional) instead of a hardcoded literal-key spec
- discriminant — the shared literal-typed property (a 'tag') that lets a switch/if narrow a union without instanceof
- contextual typing — a type inferred from the position/slot a value is used in, rather than from the value alone
- declaration merging — same-named interface declarations combine into one; type aliases and classes never merge this way
- higher-kinded type — a type constructor taking a type constructor as its own parameter (e.g. F<_>) — not directly expressible in TS
- phantom type — a type parameter that never appears in any member — carries no runtime data, exists purely to tag the type
- opaque type / branding — a type made distinguishable from its underlying primitive by an unused marker field, so plain values can't slip in unchecked