Skip to content

TypeScript lesson 8 of 9

Generics Basics

Why writing a function against any throws away type safety, how a generic type parameter lets one function work for every type while TypeScript still checks it, and how to write a simple generic constraint.

Published · Every example on this page was run before it was published.

A function that returns the first item of a list, or swaps two values, or wraps a value in a small container, does not care what type of value it is handling - the logic is identical whether the list holds numbers, strings, or anything else. Writing that function once, in a way that works for every type while TypeScript still checks every call against the type actually used, is exactly what generics are for.

The Problem: any Throws the Type Away

The obvious way to write a function that accepts "anything" is to type its parameter any. That does make it reusable, but at a real cost: any opts a value out of type checking entirely, for the rest of wherever it flows.

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

const first = firstElementAny([1, 2, 3]);
console.log(first.toUpperCase());

This compiles without a single complaint, because any tells the compiler to stop checking - first is any, and any is considered to have every method, whether or not it actually does. It only fails once it actually runs, when calling .toUpperCase() on the number 1 crashes. The type system was supposed to catch exactly this kind of mistake, and any switched it off.

The Fix: A Generic Type Parameter

A generic type parameter, written in angle brackets right after the function name, is a placeholder for "whatever type this particular call is using" - filled in fresh at every call site, and tracked properly the whole way through:

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

const first = firstElement([1, 2, 3]);
console.log(first.toUpperCase());
Output
main.ts(6,19): error TS2339: Property 'toUpperCase' does not exist on type 'number'.

Nothing about the function's logic changed - it is the same one line, returning the first item. What changed is that TypeScript now knows exactly what T is for this call: it looked at the argument, [1, 2, 3], saw every element is a number, and inferred T as number for this call alone. first is therefore typed number, and the mistake that crashed the any version is caught here before the program ever runs.

Calling With an Explicit Type Argument

Most of the time TypeScript infers T from the arguments you pass, the way it just did. You can also state it explicitly, using the same angle-bracket syntax at the call site - useful when there is nothing in the arguments alone to infer it from:

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

console.log(firstElement<string>(["red", "green", "blue"]));
console.log(firstElement([10, 20, 30]));
Output
red
10

The first call spells out <string> explicitly; the second leaves it to inference from [10, 20, 30]. Both are checked exactly the same way - only how T was decided differs.

More Than One Type Parameter

A function can take several independent generic type parameters, each inferred separately from a different argument:

TypeScript
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

console.log(pair("age", 30));
console.log(pair(true, "yes"));
Output
[ 'age', 30 ]
[ true, 'yes' ]

On the first call, A is inferred as string and B as number, purely from the two arguments actually passed - pair's definition never mentions either concrete type.

Generic Constraints

Sometimes a generic function needs to rely on something about T, without pinning it down to one concrete type. Writing <T extends ...> lets you require T to have at least certain properties, while still leaving it open to every type that has them:

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

console.log(longest("hello", "hi"));
console.log(longest([1, 2, 3], [1, 2]));
Output
hello
[ 1, 2, 3 ]

T extends { length: number } accepts any type that has a numeric length property - strings and arrays both qualify, which is why longest works on either without being written twice.

Common Mistakes

Accessing a property a bare, unconstrained T does not guarantee exists. Without an extends clause, T could be absolutely anything, so the compiler will not let you assume even something as common as .length:

TypeScript
function printLength<T>(value: T): void {
  console.log(value.length);
}
Output
main.ts(2,21): error TS2339: Property 'length' does not exist on type 'T'.

The fix is the constraint from the previous section: function printLength<T extends { length: number }>(value: T) tells the compiler exactly what it is allowed to assume about T, and the property access then type-checks.

Reaching for a generic where one concrete type would read more clearly. Generics earn their keep when a type genuinely needs to flow from a function's input through to its output, the way T does in firstElement and A/B do in pair. A function that will only ever be called with numbers gains nothing from <T> - it just makes the signature harder to read for no benefit, when function double(n: number): number already says everything that needs saying.

Assuming a generic function "remembers" the type from an earlier call. Every call to a generic function infers its own type parameters independently - nothing is shared or carried over between calls, even calls to the exact same function:

TypeScript
function identity<T>(value: T): T {
  return value;
}

console.log(identity(42));
console.log(identity("forty-two"));
Output
42
forty-two

The first call infers T as number; the second, on the very next line, infers it fresh as string. Neither call has any effect on how the other one is checked.

Next Steps

Many real implementations of binary-search are written generically, so the array can hold numbers, strings, or anything comparable - a good next step for seeing <T> used in something less abstract than a firstElement helper.

In the TypeScript playground, write your own small generic function - something like last<T>(items: T[]): T - and call it with an array of numbers and then an array of strings, checking what each call infers T to be. Then remove the <T> and replace it with any everywhere it appeared, and try to reproduce the kind of mistake this lesson opened with.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.