Typescript Generics extends

August 16, 2024

In TypeScript, the extends keyword is used with generics to create constraints. These constraints limit the types that can be used as arguments for the generic type. This ensures that the generic type must adhere to a certain structure or be a subtype of a specific type.

Generic Constraints Using extends

1. Basic Constraint with extends

You can constrain a generic type to extend from a particular type, such as an interface or a class. This ensures that the type passed as a generic must include the properties or methods defined in the constraint.

interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// Valid usages
logLength("Hello");  // Strings have a 'length' property
logLength([1, 2, 3]);  // Arrays have a 'length' property
logLength({ length: 10, value: 42 });  // Objects with a 'length' property

// Invalid usage
// logLength(42);  // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

2. Multiple Constraints

A generic type can be constrained by multiple types using intersection types (&):

interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

function logPerson<T extends HasName & HasAge>(person: T): void {
    console.log(`${person.name} is ${person.age} years old.`);
}

logPerson({ name: "Alice", age: 30 });  // Valid

// Invalid usage
// logPerson({ name: "Bob" });  // Error: Property 'age' is missing in type '{ name: string; }'.
// logPerson({ age: 25 });  // Error: Property 'name' is missing in type '{ age: number; }'.

3. Constraining with Classes

You can also constrain generics to be subclasses of a specific class.

class Animal {
    constructor(public name: string) {}
}

class Dog extends Animal {
    bark() {
        console.log("Woof!");
    }
}

function createAnimal<T extends Animal>(animal: T): T {
    console.log(animal.name);
    return animal;
}

const dog = createAnimal(new Dog("Buddy"));  // Valid
dog.bark();  // Works because dog is of type Dog

// Invalid usage
// createAnimal("Not an animal");  // Error: Argument of type 'string' is not assignable to parameter of type 'Animal'.

4. Using keyof for Constraints

You can also constrain a generic type to be a key of another type using keyof:

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

const person = { name: "Alice", age: 25 };

const name = getProperty(person, "name");  // Valid, returns "Alice"
const age = getProperty(person, "age");  // Valid, returns 25

// Invalid usage
// const invalid = getProperty(person, "address");  // Error: Argument of type '"address"' is not assignable to parameter of type 'keyof { name: string; age: number; }'.

Summary

Using extends with generics in TypeScript allows you to constrain the types that can be passed as generic arguments. This enhances type safety by ensuring that the generic type meets specific requirements, whether it's implementing an interface, being a subclass of a class, or being a key of an object. This helps to create more robust and maintainable code by enforcing the necessary structure at compile time.