JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

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

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionstypescript
PrevNext

Learn the concept

Advanced Type Patterns

typescript
senior
advanced

How do you handle complex generic constraints and variance in TypeScript?

generics
variance
constraints
covariant
contravariant
advanced
Quick Answer

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.

Detailed Explanation

Complex Constraints:

  • Multiple constraints: T extends A & B
  • Conditional constraints: T extends C ? D : E
  • Self-referential: T extends { prop: T }
  • Higher-kinded pattern emulation

Variance:

  • Covariant (out): T in output position, subtype preserving
  • Contravariant (in): T in input position, subtype reversing
  • Invariant: T in both positions, exact match required
  • Without strictFunctionTypes, function parameters are bivariant. With it enabled, function parameters are contravariant, but method declarations remain bivariant for compatibility
  • Use explicit in/out variance annotations (TS 4.7+) to document intent

Practical Implications:

  • Array<T> is covariant (can assign Dog[] to Animal[])
  • Function parameters are contravariant
  • Affects assignability of generic types

Code Examples

Complex generic constraintsTypeScript
// Multiple constraints
interface HasId { id: number; }
interface HasName { name: string; }

function merge<T extends HasId & HasName>(item: T) {
  return { ...item, displayName: `${item.name} (${item.id})` };
}

// Constraint referencing other type parameter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Conditional type in constraint
type ArrayElement<T> = T extends Array<infer E> ? E : never;

function first<T extends any[]>(arr: T): ArrayElement<T> {
  return arr[0];
}

const n = first([1, 2, 3]); // number
const s = first(['a', 'b']); // string

// Recursive constraint (JSON-like values)
type JSONValue = 
  | string 
  | number 
  | boolean 
  | null 
  | JSONValue[] 
  | { [key: string]: JSONValue };

function toJSON<T extends JSONValue>(value: T): string {
  return JSON.stringify(value);
}

Real-World Applications

Use Cases

Immutable Data Layer Design

Using variance annotations to ensure data flows safely through covariant producers and contravariant consumers

Framework Type Safety

Designing generic component/hook APIs where variance prevents unsafe type assignments

Plugin Architecture Types

Using complex constraints to ensure plugins implement required interfaces with correct variance

Mini Projects

Type-Safe Event Bus

advanced

Build an event bus using variance annotations where producers are covariant and consumers are contravariant

Fluent Query Builder

advanced

Implement a builder pattern with accumulating generic types that track selected fields

Industry Examples

TypeScript Compiler

Uses explicit variance annotations (in/out) internally for checker performance optimization

Effect-TS

Uses variance annotations extensively in its effect system for type-safe error handling

Resources

TypeScript - Generics

docs

Covariance and Contravariance in TypeScript

article

Related Questions

How do conditional types work in TypeScript?

senior
conditional-types

How do mapped types work in TypeScript?

senior
mapped-types
Previous
What are template literal types and how do you use them?
Next
How do you write declaration files for JavaScript libraries?
PrevNext