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





