Generic functions
Now the practical craft: writing generic functions that are genuinely useful. This lesson covers multiple type parameters, how inference decides them, when to specify them explicitly, and the common generic-function shapes you will actually write — because a generic that relates its inputs and outputs is where the feature pays off.
Relating inputs to outputs
The power of a generic is connecting types — the return type is expressed in terms of the input
type. The first<T>(arr: T[]): T from the last lesson does this: the return T is the array's element
T. Here are more:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1]; // element type, or undefined for an empty array
}
function wrapInArray<T>(value: T): T[] {
return [value]; // takes a T, returns a T[]
}
function pair<T>(value: T): [T, T] {
return [value, value]; // takes a T, returns a tuple of two Ts
}
last([1, 2, 3]); // number | undefined
wrapInArray("Pune"); // string[]
pair(42); // [number, number]
In each, the type parameter T threads the connection: whatever you put in determines what comes out,
precisely. This is what a generic buys over any — wrapInArray("Pune") is string[], not any[],
so the elements keep their type.
Multiple type parameters
A function can have several type parameters when it relates several types:
function pair2<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair2("Kavita", 33); // [string, number] — A is string, B is number
function mapValue<K, V, R>(map: Map<K, V>, fn: (value: V) => R): Map<K, R> {
const result = new Map<K, R>();
for (const [k, v] of map) result.set(k, fn(v));
return result;
}
pair2<A, B> has two independent type parameters — the first argument's type and the second's — and
returns a tuple relating them. Convention names them T, U, V for generic placeholders, or
meaningful single letters like K/V (key/value), R (result), E (element) when the role is clear.
Use as many type parameters as you have independent types to track, and no more — extra type parameters
are noise, exactly like extra value parameters.
Inference: TypeScript decides T for you
The reason generics are pleasant to use is that you almost never specify the type parameter — it is inferred from the arguments:
first([1, 2, 3]); // T inferred number — no <number> needed
pair2("Kavita", 33); // A inferred string, B inferred number
identity({ name: "K" }); // T inferred { name: string }
TypeScript looks at what you passed and works out each type parameter. This is the same inference you
have relied on all along, now filling in type parameters. Because of it, generic functions are called
exactly like ordinary functions — the <> machinery is invisible at the call site.
When to specify the type parameter explicitly
Occasionally inference cannot determine T, or infers something wider than you want, and you specify it
with <>:
function createArray<T>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const a = createArray(3, "x"); // T inferred string -> string[]
// when there is nothing to infer FROM, specify it:
function makeEmpty<T>(): T[] {
return [];
}
const nums = makeEmpty<number>(); // must specify — no argument to infer T from
// when inference is too wide:
const strings = identity<string>("Pune"); // usually unnecessary, but forces T = string
makeEmpty<T>() has no argument to infer from, so you must write makeEmpty<number>() or the type
would be unknown. In general: let inference work, and specify the type parameter only when there is
nothing to infer from, or when you want to override the inferred type. Writing <> on every call is
a beginner habit that adds noise — trust inference first.
Generic constraints preview, and default type parameters
Two features you will meet fully in the next lesson but should recognise:
A generic can have a default type parameter, used when neither inference nor an explicit argument provides one:
function makeArray<T = string>(): T[] {
return [];
}
const a = makeArray(); // T defaults to string -> string[]
const b = makeArray<number>(); // overridden -> number[]
And a generic can be constrained — restricted to types that have certain properties — with
extends, which is the next lesson's whole topic. For now, know that a bare <T> means "any type at
all", which is sometimes too permissive: if your function calls arr.length on T, you need to
constrain T to things that have a length. That constraint is what makes generics safe as well as
flexible.
Generic arrow functions and methods
Generics work on arrow functions and on methods too, with slightly different syntax:
const first = <T>(arr: T[]): T => arr[0]; // generic arrow function
class Box<T> { // generic class (generic-types lesson)
constructor(private value: T) {}
get(): T { return this.value; }
map<R>(fn: (value: T) => R): Box<R> { // a generic METHOD on a generic class
return new Box(fn(this.value));
}
}
The map method above has its own type parameter R (independent of the class's T), which is
common — a generic method that transforms into a new type. You will write these when building reusable
data structures, which the generic-types lesson covers.
Check your work
What makes a generic powerful. It relates inputs to outputs — the return type is expressed in terms of the input type, preserving precision.
Multiple type parameters. A function can have several (<A, B>) when it relates several independent
types; name them meaningfully (K/V/R) and use no more than needed.
How type parameters are usually determined. Inferred from the arguments — you call a generic
function like an ordinary one, no <> needed.
When to specify the type parameter explicitly. When there is nothing to infer from (makeEmpty<T>())
or when you want to override the inferred type.
Default type parameters. <T = string> — used when neither inference nor an explicit argument
provides one.
Why a bare <T> is sometimes too permissive. It means "any type", so you cannot use type-specific
features (like .length) without constraining T (next lesson).
Generic arrows and methods. Generics work on arrow functions and on methods, and a generic method can have its own type parameter independent of the class's.
Practice
- Write
last<T>,wrapInArray<T>, andpair<T>. Confirm each returns the precise type for a string and a number input. - Write
pair2<A, B>and confirmpair2("Kavita", 33)is[string, number]. - Call several generic functions with no explicit type parameter and confirm inference determines each.
- Write
makeEmpty<T>(): T[]and confirm you must specify the type parameter. Then write one with a default<T = string>and call it both ways. - Write a generic that uses
arr.lengthon a bare<T>and note whether it works. (It does for arrays; try it on a bareTvalue and see why you would need a constraint — next lesson.) - Write a generic arrow function and a generic
Box<T>class with a genericmap<R>method. - Take a function you duplicated for two types earlier and rewrite it as one generic.
Official documentation
- TypeScript — Generic Functions — Type parameters, inference, and specifying them.
- TypeScript — Working with Generic Type Variables — What you can and cannot do with a bare
T. - TypeScript — Generic parameter defaults —
<T = Default>.
Next: constraining a generic with extends.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship