← Fundamentals
Guided lesson 6 questions · 5 languages

Generics and type parameters

How one function or type works for many types, and what the compiler still checks.

A type parameter is a name for a type that each use of a function or type chooses. One piece of code then works for many types, and the compiler still checks every use with the real type. A constraint on the type parameter lists what the code may do with its values.

Before you begin

Why this matters

Arrays, promises, and most library functions you call are generic, and their error messages name type parameters. Knowing how a type argument is chosen lets you read those messages, write a function that keeps its caller’s type, and avoid any, which turns checking off without a word.

What this helps you reason about

  • Predict the type of each call’s result from its arguments.
  • Explain why a value from any can surprise you while the program runs.
  • Add the constraint that a generic function needs, and read the error when it is missing.

A kitchen cupboard holds a pile of plates and a row of mugs. Code that takes the first plate and code that takes the first mug do the same work on different types of value. Writing that code once, and still having the compiler check each use, is what type parameters are for. This lesson follows how TypeScript checks a generic function, what any loses, and what is left of the types when JavaScript runs.

Already know this? The short version
  • function first<T>(items: T[]): T works for every element type; the type argument is inferred from the call, and each use is checked with it.
  • any accepts every value but forgets its type, so a wrong use compiles; a type parameter keeps the link between argument and result.
  • Inside a generic function only what the constraint lists may be used, as in T extends { length: number }; < and > are an exception and accept any T.
  • class Pile<T> is a generic type; a Pile<number> accepts only numbers.
  • Types are erased: one JavaScript function runs for every T, and instanceof T does not compile.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
type parameter
A name for a type chosen by each call, written in angle brackets, such as T in function farthest<T extends Vehicle>(a: T, b: T): T. extends Vehicle is its constraint: T must be a type that fits Vehicle.
type argument
The type chosen for a type parameter at one use, such as number in first<number>(plates). The compiler usually infers it from the arguments.
type inference
The compiler working out the type of a name or an expression from the code, without the type being written.
any
The type that turns type checking off: a value of type any may be used as every type, and the compiler checks nothing about it.
generic type
A class, interface, or type with type parameters, such as class Pile<T>. Each use chooses the types, as in Pile<number>.
type erasure
Removing types when a program is compiled. TypeScript erases every type, type parameters included, so the JavaScript that runs has none.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Type parameters

What does a type parameter add to a function?

A function can have a type parameter: a name for a type that each call chooses. In function first<T>(items: T[]): T, T is written in angle brackets after the name. The parameter items: T[] and the result T use the same name, so the result has the type of the elements. Code with type parameters is called generic.

first(plates) uses first with T = number, so plate is a number. first(mugs) uses T = string. The type chosen for T in one call is that call's type argument. You rarely write it, because the compiler infers it from the arguments; written out, the call is first<number>(plates). A wrong one is refused: first<string>(plates) reports Argument of type 'number[]' is not assignable to parameter of type 'string[]'.

The compiler checks each use with its own type. plate.toUpperCase() reports Property 'toUpperCase' does not exist on type 'number'.

main.ts
function first<T>(items: T[]): T {
  return items[0];
}

const plates = [24, 20, 16];
const mugs = ["blue", "red"];
const plate = first(plates); // a number
const mug = first(mugs); // a string

A type parameter lets one function work for many types, and each call is checked with its own type.

Experiment

Call one function with two element types

first has a type parameter, so each call chooses the element type. The compiler works out that choice from the argument and checks the rest of the program with it.

Why it matters One generic function serves every element type, and each result keeps the type of its own elements.

Predict both lines. Then change console.log(plate + 1); to console.log(plate.toUpperCase());, and predict again.

function first<T>(items: T[]): T {
  return items[0];
}

const plates = [24, 20, 16];
const mugs = ["blue", "red"];
const plate = first(plates);
const mug = first(mugs);
console.log(plate + 1);
console.log(mug.toUpperCase());
What to look for

Before line 7 runs, plate and mug show not initialized yet. Step into first: the frame first shows items = [24, 20, 16]. The second time, the same frame name shows items = ["blue", "red"]. Back at the top level, plate shows 24 and mug shows "blue". The run prints 25, then BLUE. After the edit, nothing runs: Line 9: Property 'toUpperCase' does not exist on type 'number'.

Expected output

25
BLUE

02 / Any value

What is lost when a function takes a value of any type?

any is the type that turns checking off: a value of type any may be used as every type, and the compiler checks nothing about it. A function that takes any[] and returns any also works for plates and for mugs. But its result has lost its type.

Below, const mug: string = firstAny(plates) compiles, although the value is the number 24. mug.length then gives undefined, and nothing reports the mistake. With first<T>, the same line reports Type 'number' is not assignable to type 'string'.

A type parameter keeps the link between what goes in and what comes out; any cuts it. A type assertion such as value as string, from Static and dynamic type checks, cannot restore it, because it checks nothing while the program runs.

main.ts
function firstAny(items: any[]): any {
  return items[0];
}

const mug: string = firstAny([24, 20, 16]); // compiles
console.log(mug.length); // undefined

any forgets a value’s type through the call; a type parameter keeps it.

Coming from JavaScript: Every JavaScript function already accepts values of any type; a type parameter adds a check, not a new ability.

Precisely: unknown

unknown also accepts every value, but nothing may be done with the value until a check decides its type. It is the safe choice for a value whose type is truly not known, such as parsed JSON. A function that only passes a value on is clearer with a type parameter.

Experiment

Take the first item as any

firstAny returns any, and any may be used as every type, so the compiler accepts it as a string. Nothing checks the value while the program runs either.

Why it matters A result of type any carries no type; a mistake with it shows up later, as a strange value.

Predict both lines. Then change the first line to function firstAny<T>(items: T[]): T {, and predict again.

function firstAny(items: any[]): any {
  return items[0];
}

const plates = [24, 20, 16];
const mug: string = firstAny(plates);
console.log(mug);
console.log(mug.length);
What to look for

Step into firstAny: items shows [24, 20, 16]. Back at the top level, mug shows 24, a number, although mug is declared a string. The run prints 24, then undefined: a number has no length property. After the edit, nothing runs: Line 6: Type 'number' is not assignable to type 'string'.

Expected output

24
undefined

03 / Constraints

What may generic code do with a value of type T?

Inside first, T could be any type at all, so the code may only pass T values on. To use a property, the type parameter needs a constraint, written with extends. In longest<T extends { length: number }>(a: T, b: T): T, every T must have a length that is a number. Strings and arrays do.

Without the constraint, a.length reports Property 'length' does not exist on type 'T'. The constraint works in both directions: callers may pass only types that fit it, and the code may use only what it lists. T extends Vehicle in Interfaces, traits, and type classes was a constraint of the same kind.

TypeScript makes one exception: < and > accept two values of any type T, with no constraint. Numbers compare as numbers, and strings compare as text.

main.ts
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

console.log(longest("cup", "plate")); // plate
console.log(longest([24, 20], [16])); // [24, 20]

A constraint lists what generic code may use, and callers must pass a type that has it.

Precisely: > on objects

For two objects, > first converts each one to a primitive value, usually the text "[object Object]", so the comparison is false in both directions. A constraint such as T extends number | string keeps objects out.

04 / Generic types

How does a type get a type parameter?

A class can have type parameters too. class Pile<T> is a generic type: a pile of something, where each pile chooses what. new Pile<number>() holds plate sizes, and new Pile<string>() holds mug colours. Inside the class, T is the element type, so push(item: T) on a Pile<number> accepts only numbers. plates.push("20") reports Argument of type 'string' is not assignable to parameter of type 'number'.

You have used generic types already: number[] is a short way to write Array<number>. A type can have several type parameters, such as Pair<A, B> below, which holds two values of different types.

main.ts
class Pile<T> {
  items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }
}

class Pair<A, B> {
  constructor(public left: A, public right: B) {}
}

const plates = new Pile<number>();
const shelf = new Pair("mugs", 6); // Pair<string, number>

A generic type takes its element types as parameters, and each use is checked against the types it chose.

Experiment

Fill a pile of numbers and a pile of strings

new Pile<number>() chooses T = number for that pile, so its push takes numbers and its peek returns a number. The pile of mugs chose string.

Why it matters Each object of a generic class keeps the type argument it was created with, and the compiler checks every method call against it.

Predict both lines. Then change plates.push(20); to plates.push("20");, and predict again.

class Pile<T> {
  items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  peek(): T {
    return this.items[this.items.length - 1];
  }
}

const plates = new Pile<number>();
plates.push(24);
plates.push(20);
const mugs = new Pile<string>();
mugs.push("blue");
console.log(plates.peek() + 1);
console.log(mugs.peek());
What to look for

Each new Pile first stops in the class. The stepper names an extra frame <instance_members_initializer> there; it belongs to creating the object and can be ignored here. After line 13, plates shows {items: []}. Step into push: the frame push shows this, the Pile being filled, and item = 24; after line 5, plates shows {items: [24]}. The panel shows the items, not the type argument. The run prints 21, then blue. After the edit, nothing runs: Line 15: Argument of type 'string' is not assignable to parameter of type 'number'.

Expected output

21
blue

05 / At run time

What is left of T when the program runs?

TypeScript removes every type when it compiles to JavaScript, type parameters included. This is type erasure. The JavaScript for first is one function with no T in it, called for plates and for mugs alike. In the stepper, the frame is named first both times, and items shows only the array.

So a running program cannot ask about T. value instanceof T reports 'T' only refers to a type, but is being used as a value here. A check that must happen at run time looks at the value itself, with typeof or with instanceof and a class.

main.ts
function first<T>(items: T[]): T {
  return items[0];
}

// The JavaScript that runs has no T:
// function first(items) {
//   return items[0];
// }

Type parameters are erased: one JavaScript function runs for every T, and there is no T to check.

Coming from C#: C# keeps type arguments while the program runs; TypeScript, like Java, does not.

06 / Try it

Can you explain a fresh example?

Decide the type of each result, and what is printed.

main.ts
function pick<T>(useFirst: boolean, a: T, b: T): T {
  return useFirst ? a : b;
}

const size = pick(false, 24, 20);
const colour = pick(true, "blue", "red");
console.log(size * 2);
console.log(colour.toUpperCase());

What is printed?

Reveal the reasoning

40, then BLUE. The first call infers T = number, so size is a number, 20, and size * 2 is 40. The second infers T = string, so colour is "blue", and toUpperCase is allowed. pick never looks inside a or b; it returns one of them.

Practice tickets

  1. GEN-101 · A day of 100 litres or more never shows as the highest → Free
  2. GEN-102 · Recipes with half grams show too low a total → Sign in · Pro
  3. GEN-103 · Undo on a new speaker shows a volume it never had → Sign in · Pro

Keep exploring

Stack and heap →

Primary references