Advanced TypeScript covers variance (how subtyping flows through generics), complex generic constraints (multiple bounds, recursive types), branded types for nominal typing, and declaration merging — these patterns appear in library authoring and senior-level interview questions.
Covariant (out) for producer positions, contravariant (in) for consumer positions, invariant for both — controls how subtyping flows through generics.
Multiple bounds via intersection (T extends A & B), cross-parameter constraints (U extends T), and recursive types for tree structures.
Phantom intersection properties create nominal-like types — prevents mixing up structurally identical types like UserId and OrderId.
Same-name interfaces merge members, namespaces merge with classes/functions, and module augmentation extends third-party types.
Validates type conformance without widening — preserves literal types while checking against a broader type constraint.
Advanced TypeScript patterns go beyond basic type annotations into the type system's deeper mechanics. These concepts are essential for library authors, framework developers, and anyone working with complex type relationships.
Variance describes how subtyping relationships between types transfer through generic containers. If Dog extends Animal, does Array<Dog> extend Array<Animal>?
Array<Dog> is assignable to Array<Animal> because arrays produce values. TypeScript arrays are covariant — you can assign a Dog[] to an Animal[] variable.Animal is assignable to a variable typed as a function accepting Dog, because the Animal handler can handle any Dog. (animal: Animal) => void is assignable to (dog: Dog) => void.T is invariant.strictFunctionTypes). Both directions are allowed — this is technically unsound but pragmatic for callbacks.TypeScript 4.7 added explicit variance annotations: interface Producer<out T> (covariant), interface Consumer<in T> (contravariant), interface Both<in out T> (invariant). These improve type-checking performance and make intent clear.
Multiple constraints use intersection: T extends Serializable & Loggable requires T to satisfy both interfaces. Constraints can reference other type parameters: function merge<T, U extends T>(target: T, source: U) ensures U is a subtype of T.
Recursive type constraints model tree structures: type TreeNode<T> = { value: T; children: TreeNode<T>[] }. Self-referential conditional types enable patterns like deep readonly: type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] }.
TypeScript uses structural typing — any object with the right shape matches the type. Branded types add a phantom property to create nominal-like types:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };Now UserId and OrderId are both strings but not assignable to each other. This prevents accidentally passing a user ID where an order ID is expected. You create values through constructor functions that cast: const userId = (id: string) => id as UserId.
TypeScript merges multiple declarations with the same name in the same scope. Interfaces merge their members: two interface User declarations combine all properties. Namespaces merge with classes, functions, and enums to add static properties. Module augmentation uses declare module 'library' { ... } to add types to third-party packages.
Merging is how TypeScript's own lib.d.ts works — Array, Promise, and other built-in types are assembled from multiple interface declarations spread across different lib files.
satisfies OperatorAdded in TypeScript 4.9, satisfies validates that a value conforms to a type without widening it. const config = { port: 3000, host: 'localhost' } satisfies Config checks against Config but preserves the literal types (3000 and 'localhost' instead of number and string).
Variance controls how subtyping flows through generics — covariant for output (producers), contravariant for input (consumers). Branded types add nominal typing to TypeScript's structural system using phantom properties. Declaration merging combines same-named declarations and enables module augmentation. The satisfies operator validates type conformance without widening literal types.
Fun Fact
TypeScript's explicit variance annotations (in/out keywords added in 4.7) were inspired by C# and Kotlin, which have had them for years. The TypeScript team resisted adding them because structural typing usually infers variance correctly — but for large codebases, explicit annotations improved type-checking speed by up to 10x for deeply nested generic types.