// Generic classclass Box<T> { private contents: T; constructor(value: T) { this.contents = value; } get(): T { return this.contents; } set(value: T): void { this.contents = value; }}const stringBox = new Box<string>("hello");const numberBox = new Box(42); // Type inferred// Generic class with multiple parametersclass Pair<T, U> { constructor( public first: T, public second: U ) {} swap(): Pair<U, T> { return new Pair(this.second, this.first); }}// Generic collection classclass Collection<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } get(index: number): T { return this.items[index]; } findFirst(predicate: (item: T) => boolean): T | undefined { return this.items.find(predicate); }}
3. Generic Constraints with extends Keyword
Constraint Type
Syntax
Description
Use Case
Basic Constraint
T extends Type
T must be assignable to Type - restricts generic
Ensure minimum type requirements
Object Constraint
T extends object
T must be object type - excludes primitives
Object manipulation functions
Interface Constraint
T extends Interface
T must implement interface - ensures properties/methods
Polymorphic functions
keyof Constraint
K extends keyof T
K must be property key of T - type-safe property access
Property getters/setters
Union Constraint
T extends A | B
T must be one of specified types
Limited type options
Multiple Constraints
T extends A & B
T must satisfy multiple constraints simultaneously
Complex requirements
Example: Generic constraints
// Basic constraint - ensure property existsinterface Lengthwise { length: number;}function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); // OK - length guaranteed return arg;}logLength("hello"); // OK - string has lengthlogLength([1, 2, 3]); // OK - array has length// logLength(10); // Error - number has no length// keyof constraint for type-safe property accessfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}let person = { name: "Alice", age: 30 };let name = getProperty(person, "name"); // stringlet age = getProperty(person, "age"); // number// let x = getProperty(person, "invalid"); // Error// Multiple constraintsinterface Named { name: string; }interface Aged { age: number; }function describe<T extends Named & Aged>(obj: T): string { return `${obj.name} is ${obj.age} years old`;}// Constructor constraintfunction create<T>(constructor: new () => T): T { return new constructor();}class Person { name = "Unknown";}let person = create(Person); // Person instance
4. Default Generic Parameters
Feature
Syntax
Description
Use Case
Default Type
<T = DefaultType>
Fallback type when generic not provided
Optional configuration types
Dependent Default
<T, U = T>
Default depends on another type parameter
Related type pairs
Conditional Default
<T = T extends X ? A : B>
Default computed from conditional type
Smart defaults based on constraints
Example: Default generic parameters
// Basic defaultinterface Container<T = string> { value: T;}let c1: Container = { value: "hello" }; // T defaults to stringlet c2: Container<number> = { value: 42 }; // T explicitly number// Multiple parameters with defaultsclass Request<TData = any, TError = Error> { data?: TData; error?: TError;}// Use all defaultslet req1 = new Request(); // Request<any, Error>// Override first, use second defaultlet req2 = new Request<string>(); // Request<string, Error>// Override bothlet req3 = new Request<string, CustomError>(); // Request<string, CustomError>// Dependent default - U defaults to Tfunction create<T, U = T>(value: T): [T, U] { return [value, value as unknown as U];}// Default from conditionaltype APIResponse<T = never> = T extends never ? { status: "idle" } : { status: "success"; data: T } | { status: "error"; error: string };let response1: APIResponse = { status: "idle" };let response2: APIResponse<string> = { status: "success", data: "hello" };
5. Conditional Types with infer Keyword
Pattern
Syntax
Description
Extracts
infer in Conditional
T extends Pattern<infer U>
Extract type from pattern match
Type variable U
Function Return
(...args) => infer R
Extract function return type
Return type
Function Parameters
(infer P) => any
Extract parameter types
Parameter tuple
Array Element
(infer U)[]
Extract array element type
Element type
Promise Value
Promise<infer V>
Extract promise resolved type
Resolved type
Multiple infer
infer A, infer B
Extract multiple types from pattern
Multiple type variables
Example: infer keyword usage
// Extract return typetype GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;type Func1 = () => string;type Return1 = GetReturnType<Func1>; // string// Extract parameter typestype GetParameters<T> = T extends (...args: infer P) => any ? P : never;type Func2 = (a: string, b: number) => void;type Params = GetParameters<Func2>; // [string, number]// Extract array element typetype ElementType<T> = T extends (infer E)[] ? E : never;type Numbers = ElementType<number[]>; // number// Unwrap Promisetype Unpromise<T> = T extends Promise<infer U> ? U : T;type Resolved = Unpromise<Promise<string>>; // stringtype NotPromise = Unpromise<number>; // number// Deep unwrap nested Promisestype DeepUnpromise<T> = T extends Promise<infer U> ? DeepUnpromise<U> : T;type Deep = DeepUnpromise<Promise<Promise<number>>>; // number// Extract first array element typetype First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;type FirstType = First<[string, number, boolean]>; // string// Extract last array element typetype Last<T extends any[]> = T extends [...any[], infer L] ? L : never;type LastType = Last<[string, number, boolean]>; // boolean// Extract constructor parameter typestype ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;class MyClass { constructor(name: string, age: number) {}}type Params2 = ConstructorParams<typeof MyClass>; // [string, number]
6. Variance and Covariance in Generics
Concept
Relationship
Description
Example Context
Covariance
T<Child> ⊆ T<Parent>
Generic preserves subtype relationship - safe for output positions
Return types, readonly arrays
Contravariance
T<Parent> ⊆ T<Child>
Generic reverses subtype relationship - safe for input positions
Function parameters
Invariance
No relationship
Generic allows no subtype substitution - most restrictive
Mutable containers
Bivariance
Both directions
Generic accepts both super and subtypes - least safe
Legacy method signatures
Variance Rules
Position
Variance
Return Type
Covariant
Parameter Type
Contravariant
Readonly Property
Covariant
Writable Property
Invariant
Method (strict)
Contravariant params
Array Variance
Type
Variance
Mutable Array
Covariant (unsafe)
Readonly Array
Covariant (safe)
Tuple
Covariant elements
Example: Covariance - return types
class Animal { name: string = "";}class Dog extends Animal { breed: string = "";}// Covariance in return typestype Producer<T> = () => T;let animalProducer: Producer<Animal> = () => new Animal();let dogProducer: Producer<Dog> = () => new Dog();// Covariant - Dog producer is subtype of Animal produceranimalProducer = dogProducer; // OK - Dog extends Animal// Readonly arrays are covariant (safe)let animals: readonly Animal[] = [];let dogs: readonly Dog[] = [new Dog()];animals = dogs; // OK - readonly, so safe
Example: Contravariance - parameter types
// Contravariance in function parameterstype Consumer<T> = (arg: T) => void;let animalConsumer: Consumer<Animal> = (animal: Animal) => { console.log(animal.name);};let dogConsumer: Consumer<Dog> = (dog: Dog) => { console.log(dog.breed);};// Contravariant - Animal consumer is subtype of Dog consumerdogConsumer = animalConsumer; // OK - can handle Dog since it's an Animal// animalConsumer = dogConsumer; // Error - cannot handle all Animals
Example: Invariance - mutable containers
// Mutable arrays are technically covariant but unsafelet animals: Animal[] = [];let dogs: Dog[] = [new Dog()];animals = dogs; // OK in TypeScript (unsound)// This is why it's unsafe:animals.push(new Animal()); // Runtime error! Added Animal to Dog[]// Solution: Use readonly for true covariancefunction printAnimals(animals: readonly Animal[]) { animals.forEach(a => console.log(a.name));}printAnimals(dogs); // Safe - cannot modify array
Note: TypeScript's array covariance is unsound but pragmatic. Use readonly arrays
for type-safe covariance. Enable strictFunctionTypes for better variance checking in function
parameters.
Generics Best Practices
Use type inference when possible - avoid explicit type arguments
Apply constraints to ensure type safety and enable useful operations
Provide default types for optional flexibility
Use infer for extracting types from complex type patterns
Understand variance for safe generic type relationships