3 Topics
Chapter•80 Minutes

Generics: Reusable, Type-Safe Abstractions

Learn generic functions, constraints, defaults, keyof, and preserving relationships between input and output types.

Generic Functions and Type Parameters

Generics let a function or type operate on multiple types while preserving relationships between them. A type parameter such as T is a placeholder chosen by inference or explicitly supplied by a caller. Unlike any, a generic preserves information: if a function receives a T, its result can remain T.

Introduce generics when the same logic works for multiple types and the relationship matters. Do not make every helper generic without a reason; unnecessary type parameters can make APIs harder to understand.

Preserve the input type
function identity<T>(value: T): T {
  return value;
}

const textValue = identity("hello"); // string
const countValue = identity(42);     // number
💡 The same type parameter appears in the input and output, so the compiler preserves the relationship. The caller usually does not need to specify T because it is inferred.
Key Takeaways
  • Generics preserve relationships between values and types.
  • Inference often supplies generic arguments.
  • Prefer useful type relationships over generic-looking code.
Knowledge Check

Why is `identity<T>(value: T): T` safer than using `any`?

Generic Constraints and keyof

A generic constraint limits which types may be used for a type parameter. T extends { id: string } means T must have at least that shape while retaining any additional properties. The keyof operator produces a union of property keys, and K extends keyof T ensures that a key is valid for the given object type.

This pattern is useful for property access helpers and reusable data utilities. The relationship between T and K is important: it allows the result type to be T[K], the actual type of the selected property.

Safe property accessor
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const product = { id: "p-1", price: 99, active: true };
const price = getProperty(product, "price"); // number
// getProperty(product, "missing"); // Error
💡 K must be one of the keys of T, and the return type depends on the selected key. This prevents misspelled property names and retains the property's value type.
Key Takeaways
  • Use extends to constrain generic inputs.
  • keyof T represents valid keys of T.
  • T[K] preserves the selected property's type.
Knowledge Check

What does `K extends keyof T` guarantee?

Generic Defaults and API Design

Generic parameters can have defaults, which callers use when they do not provide an explicit type argument. Defaults should be sensible and should not hide uncertainty. For API response wrappers, a generic payload type is often useful; an error type can have a default if the library has a consistent convention.

Good generic APIs make common cases easy and unusual cases possible. Keep the number of type parameters small, order required parameters before defaulted ones, and avoid exposing internal type complexity when a simpler public contract would work.

A generic API response
type ApiResponse<TData, TError = string> =
  | { ok: true; data: TData }
  | { ok: false; error: TError };

const success: ApiResponse<{ id: string }> = {
  ok: true,
  data: { id: "u-1" }
};
💡 TData is inferred or supplied for each response. TError defaults to string, but callers can provide a structured error type when needed.
Key Takeaways
  • Generic defaults reduce repetitive type arguments.
  • Keep public generic APIs understandable.
  • Model success and failure as a discriminated union when their shapes differ.
Knowledge Check

When is a generic default used?