liveBook · Manning

Type constraint

As explained in Essential TypeScript 5, Third Edition

A generic type constraint restricts the types that may be supplied for a generic type parameter by requiring them to satisfy a specified type relationship or shape.

A generic type parameter can accept many types, but generic code cannot assume that every type has a name property. A type constraint restricts the permitted type arguments so the code can safely use the features it needs. Write the constraint after the parameter with extends. section 12.3.3

Constrain a collection by shape

In src/index.ts, `class DataCollection<T extends { name: string }>` permits `getNames()` to read `item.name`. Person, Product, and City can each be used as T because each has a name. Once `DataCollection<Person>` is created, its `add` method accepts Person objects and `getItem` returns a Person; the shape constraint does not erase that specific type. section 12.3.3

Choose a constraint that matches what the generic code actually requires:

  • `T extends (Person | Product)` limits a collection to types assignable to that union. It excludes City even when City has the name property the collection uses. section 12.3.3
  • `T extends { name: string }` expresses the required property instead. The same approach works in `interface Collection<T extends shapeType>`, where `shapeType` defines `name: string`. section 12.3.3 section 12.4
Listing 12.11 Using a shape type in the index.ts file in the src folder
import { City, Person, Product } from "./dataTypes.js";

let people = [new Person("Bob Smith", "London"), 
    new Person("Dora Peters", "New York")];
let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];
let cities = [new City("London", 8136000), new City("Paris", 2141000)];

class DataCollection<T extends { name: string }> {
    private items: T[] = [];
 
    constructor(initialItems: T[]) {
        this.items.push(...initialItems);
    }
 
    add(newItem: T) {
        this.items.push(newItem);
    }
 
    getNames(): string[] {
        return this.items.map(item => item.name);
    }
 
    getItem(index: number): T {
        return this.items[index];
    }
}

let peopleData = new DataCollection<Person>(people);
let firstPerson = peopleData.getItem(0);
console.log(`First Person: ${firstPerson.name}, ${firstPerson.city}`);
console.log(`Person Names: ${peopleData.getNames().join(", ")}`);
let productData = new DataCollection<Product>(products);
let firstProduct = productData.getItem(0);
console.log(`First Product: ${firstProduct.name}, ${firstProduct.price}`);
console.log(`Product Names: ${productData.getNames().join(", ")}`);

let cityData = new DataCollection<City>(cities);
console.log(`City  Names: ${cityData.getNames().join(", ")}`);

The src/index.ts listing shows the shape-constrained collection used with Person, Product, and City. section 12.3.3

Constraints also control where decorators may be applied. A method decorator can require a class with `getPrice(): number`, or constrain its result to `string | number` without requiring that class method. A field decorator can use `FieldType extends number` to restrict itself to number fields. For a class decorator that returns a subclass, a constructor constraint gives the compiler the type information needed to accept the replacement. section 14.2.2 section 14.3.1 section 14.3.2

A constraint does not make every change performed by a decorator visible on the original class type. If a class decorator adds `serialize()`, that method still is not part of the original Product type; an interface and type guard can provide type-safe access. section 14.3.1

How Essential TypeScript 5, Third Edition covers it

How deep it goes
Works through union and shape constraints, then applies constructor, method-result, and field constraints; does not pursue advanced generic features.
Examples
Uses named collections of people, products, and cities, plus method, class, and numeric-field decorators.
What it assumes
Assumes familiarity with TypeScript classes and properties while introducing the purpose and extends syntax of generic constraints.
What you can do after
Build it from scratch: constrain a generic collection or decorator to the types its implementation can handle.
How it connects
Connects generic type parameters and arguments with structural shapes, generic interfaces, decorators, and type guards.

Also explained in

Compare how 6 books explain type constraint →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Essential TypeScript 5, Third Edition. The section links point to the parts of the book it draws on.