Generic Functions

The Course
•
5 min read

Consider a helper that returns the first item of a list. Writing one version for strings and another for numbers repeats the same algorithm. Returning any would lose useful checking: the caller could treat a number as a string. A generic describes the relationship without choosing one fixed element type.

In function first<T>(items: readonly T[]): T | undefined, T is a type parameter. For each call, the checker chooses an element type and uses it consistently for the input and return. Given string[], the return is string | undefined. Given number[], it is number | undefined. T is not a runtime value and cannot be printed.

The caller usually does not need to write angle brackets. Inference can determine T from the arguments. Use an explicit type argument when it adds clarity or when there is not enough information, such as first<string>([]).

Generics preserve a useful relationship; they do not add capabilities. A function with unconstrained T cannot assume every item has a title or can be multiplied. It can safely return or store the item because that does not depend on its particular shape.

Use a generic only when multiple types share an algorithm and preserving the type relationship helps the caller. A domain function that only accepts sessions can simply accept Session. Fewer abstractions are often easier to understand.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}

console.log(first([10, 20]) ?? 0);

const topic = first(["Types", "Arrays"]);

console.log(topic?.toUpperCase() ?? "Empty");
console.log(first<string>([]) ?? "Empty");

Enable JavaScript to edit and type-check this example.

Check your prediction
10
TYPES
Empty

Read a real compiler error

The generic relationship is useful, but the return promises more than an arbitrary list can guarantee.

Find and fix the type errorTypeScript
function first<T>(items: readonly T[]): T {
  return items[0];
}

console.log(first([10]));

Enable JavaScript to edit and type-check this example.

Check your diagnosis

An indexed lookup may be undefined. Return T | undefined and let the caller handle absence. Do not claim the array is nonempty merely because the sample input is.

Preserve a relationship instead of returning any

A generic parameter connects positions in a contract. The function does not know which concrete T a caller chooses, so it can only perform operations justified for every T. Returning, storing or selecting an element can preserve T without assuming its fields.

A missing element is still missing after adding generics. Include undefined when the algorithm can select nothing. The useful achievement is preserving the caller’s element type on success, not pretending that every call succeeds.

Preserve a relationship instead of returning any: traceTypeScript
function last<T>(values: readonly T[]): T | undefined {
  return values[values.length - 1];
}

console.log(last([10, 20]) ?? 0);
console.log(last(["Types", "Arrays"]) ?? "Empty");
console.log(last<string>([]) ?? "Empty");

Enable JavaScript to edit and type-check this example.

Check your prediction
20
Arrays
Empty

One algorithm preserves number or string depending on the input. The empty-list branch is explicit, so callers must decide how to display absence.

Preserve a relationship instead of returning any: contrastTypeScript
function identity<T>(value: T): T {
  return value;
}

const book = identity({ title: "Types", pages: 120 });

console.log(book.title);
console.log(book.pages + 1);

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
121

Inference retains the record’s fields. Replacing T with unknown would lose safe access, while any would abandon checking. T preserves a concrete relationship without requiring the helper to inspect the record.

Try the rule on new inputs

Use the selection helper with booleans and records. Then try adding a title access inside an unconstrained helper and explain why only a constraint—not a cast—can justify it.

Your turn

Exercise

Generic Functions: practise

Write last<T>(items: readonly T[]): T | undefined. Print 30 for [10, 20, 30], Types for [“Types”], and Empty for an empty string list.

Your workspaceTypeScript
// A generic return must still account for an empty array.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The index of the last element is length minus one; an empty list still has no element.

Hint 2: a first step

Use the same T for the array element and the return union.

Show one solution
One possible solutionTypeScript
function last<T>(items: readonly T[]): T | undefined {
  return items[items.length - 1];
}

console.log(last([10, 20, 30]) ?? 0);
console.log(last(["Types"]) ?? "Empty");
console.log(last<string>([]) ?? "Empty");

Enable JavaScript to edit and type-check this example.

30
Types
Empty

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Try calling toUpperCase on the result of last([10, 20]) and read the error. Explain what information any would have lost. Then write down why the return type includes undefined even when one demonstration list happens to be nonempty.

Was this article helpful?

Your response is saved on this device.