liveBook · Manning

Generics

As explained in Essential TypeScript 5, Third Edition

A generic type is a parameterized type containing placeholders that are resolved for a particular use or object creation.

Generics let one class, method, function, or interface work with different types without giving up type checking. A type parameter is a placeholder declared in angle brackets; a type argument supplies a specific type when the code is used. This is useful for collections: JavaScript’s Map can hold mixed keys and values, while `Map<number, TodoItem>` tells TypeScript to check for numeric keys and TodoItem values. section 12.0 section 2.2.6

Write one collection for several types

In `src/index.ts`, declare `class DataCollection<T> { private items: T[] = []; constructor(initialItems: T[]) { this.items.push(...initialItems); } add(item: T) { this.items.push(item); } getItem(index: number): T { return this.items[index]; } }`. Then create `new DataCollection<Person>(people)` or `new DataCollection<Product>(products)`. Each object retains its own type argument: the first accepts and returns Person objects; the second accepts and returns Product objects. The compiler tracks the return type without an assertion or narrowing. section 12.3 section 12.3.1 section 12.3.2

A placeholder alone does not promise that `T` has a particular property. If a method reads `item.name`, declare `class DataCollection<T extends { name: string }>`: only compatible types can instantiate it, and the method can use `name`. The constraint limits eligible types; the chosen type argument still controls what each instance accepts and returns. section 12.3.3

Place and pass type parameters according to when a type must be chosen:

  • Use multiple parameters when an operation relates types, such as `DataCollection<T, U>`. Move `U` onto `collate<U>` if callers should choose a different comparison type for each call. section 12.3.4
  • A subclass can pass on, narrow, or fix a superclass’s type parameter. A generic interface can similarly leave its type open for an implementing class to pass on or fix. section 12.3.6 section 12.4.2
  • A static method cannot use an instance’s chosen type argument; it can declare its own. For example, `DataCollection.reverse<City>(cities)` supplies the static method’s argument. section 12.3.8

A runtime type check still needs runtime information

`filter<V extends T>() { return this.items.filter(item => item instanceof V); }` does not compile: `V` is a type, not a value available to `instanceof`. Generic types are removed during compilation. Instead, accept a type-predicate function, such as `filter<V extends T>(predicate: (target) => target is V): V[]`, and pass a predicate that can identify the required objects when JavaScript runs. section 12.3.7

Generics also carry types through JavaScript collections: `Set<T>` stores the collection’s item type, while `Map<string, T>` pairs string keys with those items. For a collection keyed by a selectable object property, `K extends keyof T` restricts the property name and `T[K]` supplies its value type. Mapped types use a parameter such as `Mapped<T>` to apply the same property transformation to different types. These advanced forms are useful when simpler constraints cannot describe the required relationship. section 13.2 section 13.4.4 section 13.5.2

Listing 13.13 Using an index type in a collection class in the index.ts file in the src folder
import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

//type shapeType = { name: string };

class Collection<T, K extends keyof T> implements Iterable<T> {
    private items: Map<T[K], T>;
  
    constructor(initialItems: T[] = [], private propertyName: K) {
        this.items = new Map<T[K], T>();
        this.add(...initialItems);
    }
 
    add(...newItems: T[]): void {
        newItems.forEach(newItem => 
            this.items.set(newItem[this.propertyName], newItem));
    }
 
    get(key: T[K]): T {
        return this.items.get(key);
    }
 
    get count(): number {
        return this.items.size;
    }
 
    [Symbol.iterator](): Iterator<T> {
        return this.items.values();
    }
}

let productCollection: Collection<Product, "name"> 
    = new Collection(products, "name");
console.log(`There are ${ productCollection.count } products`);

let itemByKey = productCollection.get("Hat");
console.log(`Item: ${ itemByKey.name}, ${ itemByKey.price}`);

The collection listing puts `T`, `K extends keyof T`, and `Map<T[K], T>` together, showing how a selected property determines the map’s key type. section 13.4.4

How Essential TypeScript 5, Third Edition covers it

How deep it goes
Works through parameters, arguments, constraints, inheritance, interfaces, runtime guarding, and advanced collection types.
Examples
Uses to-do items in a Map and collections of people, products, and cities, including a failed `instanceof` check.
What it assumes
Assumes familiarity with JavaScript classes and collections while introducing TypeScript type parameters and arguments.
What you can do after
Build it from scratch: define a constrained generic collection and choose type arguments for each instance.
How it connects
Connects generics to classes, interfaces, type predicates, JavaScript collections, index types, and mapped types.

Also explained in

Compare how 20 books explain generics →

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.