The problem generics solve
Suppose you write a function that returns the first element of an array:
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
It works, but only for arrays of numbers. Hand it a string[] and the compiler refuses:
const words: string[] = ["alpha", "beta"];
firstNumber(words);
error TS2345: Argument of type 'string[]' is not assignable to parameter of type 'number[]'.
Type 'string' is not assignable to type 'number'.
There are two obvious ways around it, and both are poor. You can copy the function into a firstString twin, then another for every element type, and keep them all in sync forever. Or you can type the parameter as any[], which compiles for everything but also switches off checking for whatever comes out:
function firstAny(arr: any[]): any {
return arr[0];
}
const x = firstAny(["a"]);
console.log(x.toFixed(2));
This compiles without a complaint, because x is any and anything is allowed on it. At run time it crashes with TypeError: x.toFixed is not a function, since x is a string. The types stopped helping exactly where you needed them.
A type parameter
A generic function takes a type as well as values. You declare it with angle brackets after the function name:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
T is a placeholder, a name for "whatever the element type turns out to be". It is a normal identifier; T is only a convention, and longer names such as Item work as well. Inside the function you use T like any other type. The parameter is an array of T, and the result is a T or undefined.
Each call fills the placeholder in separately:
first([7, 14, 21]); // T is number, result is number | undefined
first(["alpha", "beta"]); // T is string, result is string | undefined
You do not write the type yourself. TypeScript infers T from the argument, looking at what the array holds. You may write it explicitly, as in first<number>(nums), but you almost never need to.
What the checking gives you
Because the result type follows the argument, mistakes are caught again. Assigning the first element of a string array to a number is an error:
const wrong: number = first(["alpha"]);
error TS2322: Type 'string' is not assignable to type 'number'.
One function now has full type safety for every element type, with no copies.
Why T | undefined
Reading an index that does not exist never throws in JavaScript. On an empty array, arr[0] evaluates to undefined. The return type T | undefined records that possibility. In the compiler settings used in this lesson, undefined is quietly allowed in every type, so the compiler won't force you to check for it. Writing the type honestly still documents what callers should expect, and with stricter settings the compiler would insist on the check.
The exercise, step by step
- Declare the generic function. Write the header with
<T>after the name, a parameterarrof typeT[], and the return typeT | undefined. - Return the first element. The body is
return arr[0];. - Create a number array.
numsis anumber[]holding7,14and21. - Create a string array.
wordsis astring[]holding"alpha"and"beta". The function needs no change to accept it, which is the point of the lesson. - Print the number. Join the label
"first number = "withfirst(nums). HereTis inferred asnumber, and the+turns the number into text. - Print the word. Do the same with
"first word = "andfirst(words), whereTisstring.
The full solution
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const nums: number[] = [7, 14, 21];
const words: string[] = ["alpha", "beta"];
console.log("first number = " + first(nums));
console.log("first word = " + first(words));
The output is:
first number = 7
first word = alpha
Gotchas
- Generics exist only while compiling. The type parameter is erased, so the JavaScript that runs is just
function first(arr) { return arr[0]; }. A generic does not change what the code does, only what the compiler checks. - Empty arrays.
first([])givesundefinedat run time. The text"first = " + first(e)for an emptyewould printfirst = undefined. - Mixed arrays.
first([1, "a"])is accepted:Tbecomesstring | number. - Prefer inference. Writing
first<string>(nums)by hand with the wrong type is an error, and inference makes it unnecessary.
Key takeaways
<T>after a function name declares a type placeholder that is filled in at every call.- TypeScript infers
Tfrom the arguments, so callers normally write nothing extra. - A generic keeps type safety for every element type, which
any[]does not. - Indexing an empty array gives
undefined, soT | undefineddescribes the result accurately.