TypeScript Generics
1. Generic Functions () and Method Signatures
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| Generic Function | function<T>(arg: T): T |
Type parameter allows function to work with multiple types | Reusable functions, identity operations |
| Multiple Type Params | <T, U, V> |
Function can accept multiple generic types | Complex transformations, tuple operations |
| Arrow Function Generic | <T>(arg: T) => T |
Generic arrow function syntax | Callbacks, inline functions |
| Generic Method | method<T>(arg: T) |
Class method with type parameter | Flexible class methods |
| Type Inference | Implicit from arguments | TypeScript infers generic type from usage - no explicit type needed | Clean API usage, reduced verbosity |
Example: Generic functions
// Basic generic function
function identity<T>(arg: T): T {
return arg;
}
// Usage with explicit type
let output1 = identity<string>("hello");
// Usage with type inference (preferred)
let output2 = identity("hello"); // T inferred as string
// Generic with array
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
let first = firstElement([1, 2, 3]); // number | undefined
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
let p = pair("age", 30); // [string, number]
// Generic arrow function
const map = <T, U>(arr: T[], fn: (item: T) => U): U[] => {
return arr.map(fn);
};
// Generic with constraints
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
Example: Generic methods in classes
class DataStore {
private data: any[] = [];
// Generic method
add<T>(item: T): void {
this.data.push(item);
}
get<T>(index: number): T {
return this.data[index];
}
filter<T>(predicate: (item: T) => boolean): T[] {
return this.data.filter(predicate);
}
}
const store = new DataStore();
store.add<string>("hello");
store.add<number>(42);
2. Generic Interfaces and Generic Classes
| Feature | Syntax | Description | Example |
|---|---|---|---|
| Generic Interface | interface Name<T> { } |
Interface with type parameter - reusable contracts | interface Box<T> |
| Generic Class | class Name<T> { } |
Class with type parameter - typed containers | class Stack<T> |
| Generic Type Alias | type Name<T> = ... |
Type alias with parameter - composable types | type Result<T> |
| Implements Generic | implements Interface<T> |
Class implements generic interface with specific or generic type | implements Comparable<T> |
Example: Generic interfaces
// Generic interface
interface Container<T> {
value: T;
getValue(): T;
setValue(val: T): void;
}
// Implement with specific type
class StringContainer implements Container<string> {
constructor(public value: string) {}
getValue(): string {
return this.value;
}
setValue(val: string): void {
this.value = val;
}
}
// Generic function interface
interface Transformer<T, U> {
(input: T): U;
}
const toNumber: Transformer<string, number> = (str) => parseInt(str);
// Generic interface with multiple type parameters
interface KeyValuePair<K, V> {
key: K;
value: V;
}
Example: Generic classes
// Generic class
class 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 parameters
class Pair<T, U> {
constructor(
public first: T,
public second: U
) {}
swap(): Pair<U, T> {
return new Pair(this.second, this.first);
}
}
// Generic collection class
class 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 (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 exists
interface 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 length
logLength([1, 2, 3]); // OK - array has length
// logLength(10); // Error - number has no length
// keyof constraint for type-safe property access
function 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"); // string
let age = getProperty(person, "age"); // number
// let x = getProperty(person, "invalid"); // Error
// Multiple constraints
interface 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 constraint
function 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 default
interface Container<T = string> {
value: T;
}
let c1: Container = { value: "hello" }; // T defaults to string
let c2: Container<number> = { value: 42 }; // T explicitly number
// Multiple parameters with defaults
class Request<TData = any, TError = Error> {
data?: TData;
error?: TError;
}
// Use all defaults
let req1 = new Request(); // Request<any, Error>
// Override first, use second default
let req2 = new Request<string>(); // Request<string, Error>
// Override both
let req3 = new Request<string, CustomError>(); // Request<string, CustomError>
// Dependent default - U defaults to T
function create<T, U = T>(value: T): [T, U] {
return [value, value as unknown as U];
}
// Default from conditional
type 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 type
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Func1 = () => string;
type Return1 = GetReturnType<Func1>; // string
// Extract parameter types
type 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 type
type ElementType<T> = T extends (infer E)[] ? E : never;
type Numbers = ElementType<number[]>; // number
// Unwrap Promise
type Unpromise<T> = T extends Promise<infer U> ? U : T;
type Resolved = Unpromise<Promise<string>>; // string
type NotPromise = Unpromise<number>; // number
// Deep unwrap nested Promises
type DeepUnpromise<T> = T extends Promise<infer U>
? DeepUnpromise<U>
: T;
type Deep = DeepUnpromise<Promise<Promise<number>>>; // number
// Extract first array element type
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type FirstType = First<[string, number, boolean]>; // string
// Extract last array element type
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type LastType = Last<[string, number, boolean]>; // boolean
// Extract constructor parameter types
type 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 |
Example: Covariance - return types
class Animal {
name: string = "";
}
class Dog extends Animal {
breed: string = "";
}
// Covariance in return types
type Producer<T> = () => T;
let animalProducer: Producer<Animal> = () => new Animal();
let dogProducer: Producer<Dog> = () => new Dog();
// Covariant - Dog producer is subtype of Animal producer
animalProducer = 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 parameters
type 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 consumer
dogConsumer = 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 unsafe
let 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 covariance
function 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
- Prefer
readonlyfor covariant collections