JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeTopicstypescriptAdvanced Type Patterns
Next
typescript
advanced
12 min read

Advanced Type Patterns

advanced
branded-types
constraints
contravariant
covariant
declaration-merging
generics
satisfies
variance

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.

Key Points

1Variance

Covariant (out) for producer positions, contravariant (in) for consumer positions, invariant for both — controls how subtyping flows through generics.

2Complex Constraints

Multiple bounds via intersection (T extends A & B), cross-parameter constraints (U extends T), and recursive types for tree structures.

3Branded Types

Phantom intersection properties create nominal-like types — prevents mixing up structurally identical types like UserId and OrderId.

4Declaration Merging

Same-name interfaces merge members, namespaces merge with classes/functions, and module augmentation extends third-party types.

5satisfies Operator

Validates type conformance without widening — preserves literal types while checking against a broader type constraint.

What You'll Learn

  • Explain covariance, contravariance, and invariance in TypeScript generics
  • Use branded types to create nominal typing in a structural type system
  • Understand declaration merging and module augmentation patterns
  • Apply the satisfies operator for type validation without widening

Deep Dive

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

Variance describes how subtyping relationships between types transfer through generic containers. If Dog extends Animal, does Array<Dog> extend Array<Animal>?

  • Covariant (output position): Array<Dog> is assignable to Array<Animal> because arrays produce values. TypeScript arrays are covariant — you can assign a Dog[] to an Animal[] variable.
  • Contravariant (input position): For function parameters in strict mode, the relationship reverses. A function accepting 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.
  • Invariant: When a type appears in both input and output positions, it must match exactly. A mutable container that both reads and writes T is invariant.
  • Bivariant: TypeScript's default for method parameters (without 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.

Complex Generic Constraints

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] }.

Branded Types (Nominal Typing)

TypeScript uses structural typing — any object with the right shape matches the type. Branded types add a phantom property to create nominal-like types:

TypeScript
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.

Declaration Merging

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.

The satisfies Operator

Added 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).

Key Interview Distinction

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.

Learn These First

Generics

intermediate

Interfaces vs Type Aliases

beginner

Conditional Types

advanced

Continue Learning

Mapped Types

advanced

Template Literal Types

advanced

Practice What You Learned

How do you handle complex generic constraints and variance in TypeScript?
senior
advanced
Complex constraints use multiple extends, conditional types, and recursive patterns. Variance determines how type parameters relate in subtyping: covariant (out), contravariant (in), invariant (both), and bivariant (neither). Understanding variance helps design safe generic APIs.
Next
TypeScript Fundamentals
Next