TypeScript Generic Constraints

Limit generic types with constraints

🔗 What are Generic Constraints?

Generic constraints limit what types can be used with generics using the extends keyword. They ensure type parameters have specific properties or capabilities. This provides flexibility while maintaining type safety and preventing errors from missing properties or methods.


interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(item: T): void {
    console.log(item.length);
}
                                    

Constraint Types

📏

Interface Constraints

Require specific properties

interface Named {
    name: string;
}

function greet<T extends Named>(obj: T) {
    console.log(obj.name);
}
🔑

Keyof Constraints

Limit to object keys

function getProperty<T, K extends keyof T>
    (obj: T, key: K) {
    return obj[key];
}
🏗️

Class Constraints

Require constructor types

function create<T>(c: new () => T): T {
    return new c();
}
🔢

Multiple Constraints

Combine multiple requirements

function process<T extends Named & HasId>
    (item: T) {
    // item has name and id
}

🔹 Basic Constraints

Constrain generics to types with specific properties:

// Interface defining the constraint
interface HasLength {
    length: number;
}

// Function that requires length property
function logLength<T extends HasLength>(item: T): void {
    console.log(`Length: ${item.length}`);
}

// These work - they have length property
logLength("hello");           // string has length
logLength([1, 2, 3]);         // array has length
logLength({ length: 10 });    // object with length

// This would error - number doesn't have length
// logLength(42);  // ❌ Error

Output:

Length: 5
Length: 3
Length: 10

🔹 Keyof Constraints

Ensure a type parameter is a key of another type:

// Get property value safely
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person = {
    name: "Alice",
    age: 30,
    city: "NYC"
};

console.log(getProperty(person, "name"));  // Alice
console.log(getProperty(person, "age"));   // 30
console.log(getProperty(person, "city"));  // NYC

// This would error - "salary" is not a key of person
// console.log(getProperty(person, "salary"));  // ❌ Error

Output:

Alice
30
NYC

🔹 Interface Constraints

Require objects to implement specific interfaces:

interface Identifiable {
    id: number;
}

interface Named {
    name: string;
}

// Function requiring both interfaces
function displayItem<T extends Identifiable & Named>(item: T): void {
    console.log(`ID: ${item.id}, Name: ${item.name}`);
}

const user = { id: 1, name: "John", email: "[email protected]" };
const product = { id: 101, name: "Laptop", price: 999 };

displayItem(user);     // ID: 1, Name: John
displayItem(product);  // ID: 101, Name: Laptop

// This would error - missing required properties
// displayItem({ id: 1 });  // ❌ Error: missing name
// displayItem({ name: "Test" });  // ❌ Error: missing id

Output:

ID: 1, Name: John
ID: 101, Name: Laptop

🔹 Class Type Constraints

Work with constructor functions and create instances:

// Base class
class Animal {
    constructor(public name: string) {}
    
    makeSound(): void {
        console.log("Some sound");
    }
}

class Dog extends Animal {
    makeSound(): void {
        console.log(`${this.name} barks: Woof!`);
    }
}

class Cat extends Animal {
    makeSound(): void {
        console.log(`${this.name} meows: Meow!`);
    }
}

// Function that creates instances of Animal subclasses
function createAnimal<T extends Animal>(
    AnimalClass: new (name: string) => T,
    name: string
): T {
    return new AnimalClass(name);
}

const dog = createAnimal(Dog, "Buddy");
const cat = createAnimal(Cat, "Whiskers");

dog.makeSound();  // Buddy barks: Woof!
cat.makeSound();  // Whiskers meows: Meow!

Output:

Buddy barks: Woof!
Whiskers meows: Meow!

🔹 Comparing Objects

Use constraints to safely compare objects:

interface Comparable {
    compareTo(other: Comparable): number;
}

class Person implements Comparable {
    constructor(public name: string, public age: number) {}
    
    compareTo(other: Person): number {
        return this.age - other.age;
    }
}

function findMin<T extends Comparable>(items: T[]): T | undefined {
    if (items.length === 0) return undefined;
    
    let min = items[0];
    for (let i = 1; i < items.length; i++) {
        if (items[i].compareTo(min as any) < 0) {
            min = items[i];
        }
    }
    return min;
}

const people = [
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
];

const youngest = findMin(people);
console.log(`Youngest: ${youngest?.name}, Age: ${youngest?.age}`);

Output:

Youngest: Bob, Age: 25

🔹 Array Operations with Constraints

Create type-safe array utilities:

interface HasId {
    id: number;
}

// Find item by ID
function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
}

// Remove item by ID
function removeById<T extends HasId>(items: T[], id: number): T[] {
    return items.filter(item => item.id !== id);
}

const users = [
    { id: 1, name: "Alice", role: "admin" },
    { id: 2, name: "Bob", role: "user" },
    { id: 3, name: "Charlie", role: "user" }
];

const user = findById(users, 2);
console.log(user);  // { id: 2, name: 'Bob', role: 'user' }

const remaining = removeById(users, 2);
console.log(remaining.length);  // 2
console.log(remaining.map(u => u.name));  // ['Alice', 'Charlie']

Output:

{ id: 2, name: 'Bob', role: 'user' }
2
['Alice', 'Charlie']

💡 Key Points:

  • Use extends keyword to add constraints: <T extends Type>
  • keyof constraint ensures type is a valid key: <K extends keyof T>
  • Combine multiple constraints with & : <T extends A & B>
  • Constructor constraints: new () => T
  • Constraints provide type safety while maintaining flexibility
  • Use constraints to access properties safely in generic code

🧠 Test Your Knowledge

What keyword is used to add constraints to generic types?