Course outline · 0% complete

0/26 lessons0%

Course overview →

Your First Generic Function

lesson 6-1 · ~11 min · 18/26

Suppose you need firstOf(items) to work on both number[] and string[], and the tempting shortcut is to type the parameter as any[].

The problem is that the return value becomes any, so type checking is lost exactly where you need it. As lesson 2-3 established, any switches checking off. firstOf would accept anything and return any, so misuse like firstOf([1, 2]).toUpperCase() compiles cleanly and then crashes at runtime.

That is the worst of both worlds: the function is flexible, but the flexibility costs you the guarantee. Generics solve the same problem without losing the types, which is what this unit is about.

The problem generics solve

Here is a function that works for any element type:

function firstOf(items: number[]): number {
  return items[0];
}

For strings you would need a copy with string[]. Copies for every type, all identical bodies. The any escape hatch kills checking. What you want to say is: "this works for some type, call it T, and whatever array of T comes in, one T comes out".

That is exactly a generic function:

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

<T> declares a type parameter, a placeholder filled in at each call. Call it with [10, 20, 30] and T becomes number for that call. Call it with strings and T becomes string. One body, full checking, no copies.

firstOf<>(items)numberfirstOf([10, 20, 30]) → T = numberstringfirstOf(["alpha", "beta"]) → T = stringthe T slot is filled in at each call site
A generic function has a slot. Each call fills the slot with a concrete type, inferred from the argument.

One generic function, two element types

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

const n = firstOf([10, 20, 30]);
const s = firstOf(["alpha", "beta"]);

console.log(n + 5);
console.log(s.toUpperCase());

Output

15
ALPHA

n is a number and s is a string, both inferred, which is why n + 5 and s.toUpperCase() each compile. The single function body served two different types without any loss of precision.

Two things to take from this

  • Writing firstOf<number>([10, 20, 30]) explicitly is legal but almost never necessary. TypeScript infers T from the argument, so generic calls usually look exactly like ordinary calls.
  • firstOf([1, 2]).toUpperCase() is a compile error, which is the whole contrast with the any[] version. Checking survives the trip through the generic function.

lastOf: the same pattern with a different index

lastOf<T> returns the final element of any array, reading the element at index items.length - 1.

function lastOf<T>(items: T[]): T {
  return items[items.length - 1];
}

console.log(lastOf([1, 2, 3]));
console.log(lastOf(["x", "y", "z"]).toUpperCase());

Output

3
Z

Reading the generic signature

  • The shape copies firstOf exactly, changing only the index expression. <T> goes after the function name, the parameter is T[], and the return type is T.
  • The second call chains .toUpperCase() because T was inferred as string there. On the first call T is number, and the same chained method would be rejected.
  • The type parameter connects input to output. That link is the thing any[] throws away and generics preserve.