---
title: Generics
source: 'https://academia.sh/en/courses/typescript/generics'
course: TypeScript
language: en
updated: '2026-08-17T18:09:53+00:00'
license: 'CC BY-SA 4.0'
---

# Generics

Writing shape-independent structures with type parameters, type argument inference, constraints, the keyof constraint, default type parameters, and generic misuse.

Every type in the previous topic was fixed: a `Log` holding `Measurement` records, a
`select` function filtering `Measurement`. Yet a log's structure is independent of the
record type — the same code should be able to collect measurements, warnings, or
events.

There are two bad ways to get this. Writing a separate log for every record type
duplicates code; letting the log use `any[]` removes type safety. **Generics** give a
third way: writing the structure with a **type parameter** and supplying the type at
the point of use.

## Type-Parameterized Structures

A type parameter is written in angle brackets after the declaration's name and used
like a type throughout the body:

```typescript
interface Measurement {
  id: string;
  value: number;
}

class Log<T> {
  private records: T[] = [];

  add(record: T): void {
    this.records.push(record);
  }

  last(): T | undefined {
    return this.records.at(-1);
  }

  get count(): number {
    return this.records.length;
  }
}

const measurements = new Log<Measurement>();
measurements.add({ id: "s-01", value: 21.4 });
console.log(measurements.last()?.value, measurements.count);

const warnings = new Log<string>();
warnings.add("sensor not responding");
console.log(warnings.last()?.toUpperCase());
```

Output:

```text
21.4 1
SENSOR NOT RESPONDING
```

A single class was used with two different record types, and type information was kept
at every use: the call `measurements.last()` gives `Measurement | undefined`, the call
`warnings.last()` gives `string | undefined`. If the line
`measurements.add("s-02");` is added at the end of the file, after a blank line:

```text
m1.ts(30,18): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Measurement'.
```

An `any[]` solution would not have produced this diagnostic. Generics are the way to
share a common structure without losing type safety.

The return type being `T | undefined` is also part of the design: an `at(-1)` call on
an empty array gives `undefined`, and the type records that. The caller is forced to
handle that possibility with `?.`.

## Type Argument Inference

The type argument is often left unwritten; the compiler infers it from the arguments:

```typescript
function first<T>(records: readonly T[]): T | undefined {
  return records[0];
}

const n = first([21.4, 22.1]);
const s = first(["s-01", "s-02"]);
console.log(n?.toFixed(1), s?.toUpperCase());
```

The output is `21.4 S-01`.

There is no need to write `first<number>` or `first<string>`. The declared type of `n`
is inferred as `number | undefined`, the type of `s` as `string | undefined`, and each
one's methods are available accordingly.

For inference to work, the type parameter must **appear in the arguments**. A type
parameter that appears only in the return type cannot be inferred; the danger of this
case is covered at the end of the lesson.

## Constraints

A type parameter is a type about which nothing is known; no operation can be performed
on it. To access specific members, a **constraint** is written on the parameter:

```typescript
interface Identified {
  id: string;
}

function collectIds<T extends Identified>(records: readonly T[]): string[] {
  return records.map((r) => r.id);
}

console.log(collectIds([{ id: "s-01", value: 21.4 }]));
console.log(collectIds([{ name: "boiler-2" }]));
```

```text
m4.ts(10,27): error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'Identified'.
```

The declaration `T extends Identified` does two things: it makes the `r.id` access in
the body possible, and it rejects non-conforming arguments at the call site. The
acceptance of the ninth line also matters — the `value` field is extra, and a
constraint only states the minimum requirement.

Without the constraint, writing `r.id` would have been rejected by the compiler: `T`
could be any type, a string as much as a number.

## Constraining with `keyof`

Constraints can also refer to other type parameters. The most useful form is built with
the `keyof` operator, which gives a type's field names:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

function field<T, A extends keyof T>(record: T, name: A): T[A] {
  return record[name];
}

const record: Measurement = { id: "s-01", value: 21.4, unit: "C" };
const v: number = field(record, "value");
const u: "C" | "Pa" | "%" = field(record, "unit");
console.log(v, u);

field(record, "location");
```

```text
m5.ts(16,15): error TS2345: Argument of type '"location"' is not assignable to parameter of type 'keyof Measurement'.
```

Three new notations appear here. `keyof T` is a union made of literal types of `T`'s
field names — here `"id" | "value" | "unit"`. The constraint `A extends keyof T` limits
the second argument to those names. `T[A]` is an **indexed access type**: it gives the
type of the field named `A`.

The result is a return type that varies by field name. The call `field(record,
"value")` has type `number`, the call `field(record, "unit")` has type `"C" | "Pa" |
"%"`. Without writing an overload, as many distinct signatures as there are fields have
been obtained.

## Default Type Parameters

A type parameter can be given a default value. A type carrying the result of a
boundary validation is a typical use of this:

```typescript
type Result<D, E = string> =
  | { status: "success"; value: D }
  | { status: "error"; error: E };

interface Measurement {
  id: string;
  value: number;
}

function parse(raw: string): Result<Measurement> {
  const parts = raw.split("=");
  if (parts.length !== 2) {
    return { status: "error", error: `invalid format: ${raw}` };
  }
  const value = Number(parts[1]);
  if (!Number.isFinite(value)) {
    return { status: "error", error: `not a number: ${parts[1]}` };
  }
  return { status: "success", value: { id: parts[0], value } };
}

function format(result: Result<Measurement>): string {
  return result.status === "success"
    ? `${result.value.id}=${result.value.value}`
    : `error: ${result.error}`;
}

console.log(format(parse("s-01=21.4")));
console.log(format(parse("broken")));
console.log(format(parse("s-02=abc")));
```

Output:

```text
s-01=21.4
error: invalid format: broken
error: not a number: abc
```

In `Result<Measurement>`, the second type argument is not given; the default `string`
is used. When the error type needs detail, `Result<Measurement, ErrorCode>` is written
and the code outside `format` does not change.

This type is the matured form of the boundary rule set in the first lesson. The `parse`
function neither throws an exception nor returns `null`; it carries failure as **a
member of the type**, and the compiler forces the caller to handle it. The
discriminated union has turned into an error-handling tool here.

## Misusing Generics

Generics do not improve every situation. There are two counter-examples.

**A type parameter that appears only once.** In the signature `function
print<T>(value: T): void`, `T` appears in only one parameter and forms no relationship;
writing `unknown` does the same job more explicitly. The value of a generic lies in
forming a **link** between two points: an input type and an output type, or two
parameters.

**A type parameter that appears only in the return type.** This is a hidden type
assertion:

```typescript
function unsafe<T>(raw: unknown): T {
  return raw as T;
}

const label = unsafe<string>(42);
console.log(label.toUpperCase());
```

The file **passes** the type check; when run, however:

```text
TypeError: label.toUpperCase is not a function
```

Whatever the caller writes for `T`, the compiler accepts it, because the `as T` in the
body silences the check. This signature looks like it promises validation while
validating nothing — it is more dangerous than returning `any`, because it looks type
safe.

Rule: **a type parameter must appear in at least two places.** If it appears only in
the return type, the signature should return `unknown` and the caller should narrow it
themselves.

## Summary

- A type parameter lets a structure's shape be written independently of the record
  type, and unlike an `any` solution, it preserves type safety.
- Type arguments are usually inferred from call arguments; inference requires the
  parameter to appear in the argument list.
- A constraint (`T extends K`) allows operations on a type parameter and rejects
  non-conforming arguments at the call site.
- `keyof T` gives the union of field names, `T[A]` the indexed access type; together
  they build return types that vary by field name.
- A default type parameter shortens the common case; a result type built with a
  discriminated union carries failure as a member of the type.
- A type parameter must appear in at least two places; a parameter appearing only in
  the return type is a hidden type assertion.

## Next Step

Several times in this lesson, a test was performed on a union type and the compiler
narrowed it down to a single member. The rules of this narrowing have not yet been laid
out: which tests count as proof, how far the proof carries, how the result of an
assertion function you write yourself is communicated to the compiler. The next lesson
covers narrowing in detail.
