After this lesson you can
- Write a function that keeps the caller's type
- Constrain a type parameter so you can use it
- Read the common utility types without looking them up
This function works on anything and tells you nothing:
function first(items: any[]): any { return items[0]; }
A type parameter keeps the connection between what went in and what comes out:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined
T is not a type; it is a slot the caller fills, usually without noticing,
because TypeScript infers it from the argument.
Constraints
Inside the function you may only do what every possible T allows,
which is almost nothing. extends narrows what a caller may pass, and in
return lets you use it:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("abc", "de"); // fine: strings have length
longest([1, 2], [3]); // fine: arrays have length
Try it
// One entry per key; a later item with the same key replaces an earlier one.function indexBy<T, K extends string>(items: T[], key: (item: T) => K): Record<K, T> { const out = {} as Record<K, T>; for (const item of items) { out[key(item)] = item; } return out;} export function run(): string[] { const people = [ { login: "nino", name: "Nino Beridze", country: "GE" }, { login: "luka", name: "Luka Meladze", country: "DE" }, ]; const byLogin = indexBy(people, (p) => p.login); // byLogin.nino is known to be one of those objects, not any. return [byLogin.nino.name, byLogin.luka.country];}The utility types worth knowing
Partial<T>— every property optional. A patch object.Required<T>— the opposite.Pick<T, K>/Omit<T, K>— a subset of the properties.Record<K, V>— an object with keysKand valuesV.ReturnType<F>— what a function type returns.
They are all declared in TypeScript's own library files, built from generics plus the mapped and conditional types a later lesson covers. Nothing here is magic.
Where a generic signature can lie
indexBy above returns Record<K, T>, and K was inferred as string
— so the type promises an entry for every string key. Ask for one
that does not exist:
function indexBy<T, K extends string>(items: T[], key: (item: T) => K): Record<K, T> { const out = {} as Record<K, T>; for (const item of items) { out[key(item)] = item; } return out;} export function run(): string[] { const people = [ { login: "nino", name: "Nino Beridze" }, { login: "luka", name: "Luka Meladze" }, ]; const byLogin = indexBy(people, (p) => p.login); // Record<string, ...> promises an entry for every string key. There is none for ana. const ana = byLogin["ana"]; const answer: string[] = [typeof ana]; try { answer.push(ana.name); } catch (e) { answer.push(String(e)); } return answer;}The type says T; the value is undefined. The {} as Record<K, T>
inside the function was the first lie, and indexing a Record with any
string is the second. The noUncheckedIndexedAccess compiler flag (not part
of strict) makes every such index read T | undefined, which forces the
check.
Common mistakes
- A type parameter used once.
function log<T>(x: T): voidgains nothing overx: unknown. A type parameter earns its place by linking two positions — an input to an output, or two inputs to each other. - Constraining too little, then casting. Writing
<T>and then(item as any).lengthinside throws the check away;T extends { length: number }keeps it. - Trusting
Record<string, X>lookups. As above, a missing key comes backundefinedwhile the type saysX.
What interviewers ask
- What does
T extends { length: number }mean? A constraint: callers may only pass types with a numeric , and in return the body may use .
Exercise · Pro
The longest of anything with a length
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade