Generics and Generic Programming

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

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 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 readonly for covariant collections