Skip to content
Home
TypeScript Generics: Reusable Type-Safe Components and Patterns

TypeScript Generics: Reusable Type-Safe Components and Patterns

TypeScript TypeScript 7 min read 1455 words Beginner ExcellentWiki Editorial Team

Generics are the backbone of TypeScript’s type system, enabling reusable components that work across a variety of types without sacrificing type safety. They allow you to write functions, classes, and interfaces that defer the specification of one or more types until the component is used. Microsoft’s TypeScript Handbook describes generics as “the ability to create components that can work over a variety of types rather than a single one” (see TypeScript Generics). This guide covers generic functions, constraints, conditional types, mapped types, and advanced patterns used in production systems.

Generic Functions and Identity Patterns

The simplest generic function is an identity — it returns whatever type it receives. The generic type parameter <T> captures the caller’s type and propagates it to the return type.

function identity<T>(arg: T): T {
  return arg;
---

When you call identity(42), TypeScript infers T as number. The compiler enforces that the argument and return type match, catching mismatches at compile time. Generic functions eliminate any and preserve type information across operations, which is essential for library authors building collection utilities, state management helpers, and API client wrappers.

Working with Arrays and Promises

Generics shine when handling collections and asynchronous operations. Consider a generic async wrapper:

async function wrapInPromise<T>(value: T): Promise<T> {
  return value;
---

// Usage — type is preserved
const result = await wrapInPromise({ name: 'Alice', age: 30 });
// result is { name: string; age: number }

Without generics, wrapInPromise would return Promise<any>, requiring manual type assertions at every call site. The generic version automatically propagates the exact type throughout the chain.

Type Parameter Inference vs Explicit Annotation

TypeScript’s type inference often derives generic parameters automatically. However, when inference fails — such as with complex callback signatures or partial application — explicit annotation is necessary:

const result = identity<string>('hello'); // explicit
const inferred = identity('hello');        // inferred as string

The TypeScript compiler’s inference algorithm works backward from usage to type parameters. According to Basarat’s TypeScript guide, understanding inference order helps predict when explicit annotations are required (see Basarat Generics).

Generic Constraints with extends

Constraints restrict the types a generic parameter can accept using the extends keyword. This ensures the type has required properties or methods.

interface HasLength {
  length: number;
---

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
---

Without the constraint, accessing arg.length would be a type error. Constraining T to HasLength allows the function to accept strings, arrays, and any object with a length property while rejecting numbers, booleans, and plain objects.

Keyof Constraints

Combining generics with keyof enables type-safe property accessors:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
---

This pattern is widely used in ORMs for type-safe query builders and in state management libraries for typed selectors. The return type T[K] is a lookup type that resolves to the actual type of the specified property.

Generic Constraints with Classes

Classes can enforce constraints on their type parameters, enabling polymorphic behavior without runtime type checks:

interface HasId {
  id: string;
---

class Repository<T extends HasId> {
  private items = new Map<string, T>();

  add(item: T): void {
    this.items.set(item.id, item);
  }

  get(id: string): T | undefined {
    return this.items.get(id);
  }

  update(id: string, updates: Partial<T>): T | undefined {
    const existing = this.items.get(id);
    if (!existing) return undefined;
    const updated = { ...existing, ...updates };
    this.items.set(id, updated);
    return updated;
  }
---

Variance in Generics

TypeScript’s generic types follow structural typing rules for variance — the relationship between complex types based on their component types:

  • Covariant: Producer<T> is covariant in T if Producer<Dog> is assignable to Producer<Animal>. Read-only collections and Promise types are covariant.
  • Contravariant: Consumer<T> is contravariant if Consumer<Animal> is assignable to Consumer<Dog>. Function parameters are contravariant under strictFunctionTypes.
  • Invariant: MutableBox<T> is invariant if neither direction is assignable. Mutable arrays and writable properties are invariant.

Understanding variance is essential for library design. For example, a read-only array (ReadonlyArray<Dog>) is safely assignable to ReadonlyArray<Animal> (covariant), but a writable array is not — writing an Animal into a Dog[] would violate type safety. The TypeScript team formalizes variance through the --strictFunctionTypes flag, which makes function parameters contravariant and catches unsound assignments at compile time.

Conditional Types

Conditional types select one of two types based on a condition expressed as a type relationship. Their syntax mirrors ternary expressions: T extends U ? X : Y.

type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'hello'>; // 'yes'
type B = IsString<42>;      // 'no'

Conditional types unlock distributive behavior when a union is passed. If T is string | number, IsString<T> distributes over each member, yielding 'yes' | 'no'. This is the mechanism behind many utility types.

Filtering Union Types

type ExtractStrings<T> = T extends string ? T : never;
type Strings = ExtractStrings<string | number | boolean>; // string

never acts as a filter, removing unwanted members from the union. TypeScript’s built-in Extract and Exclude utilities work exactly this way.

The infer Keyword

infer within conditional types introduces a new type variable to capture a portion of a conditional match. It is the foundation for type extraction utilities.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

The built-in ReturnType, Parameters, InstanceType, and ConstructorParameters all use infer. For example, extracting the resolved value type from a Promise:

type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Result = Unwrap<Promise<string>>; // string

Mapped Types

Mapped types transform existing types by iterating over their keys. They are essential for creating variations of types without duplication.

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
---;

The [P in keyof T] syntax iterates over every key of T, applying the readonly modifier. The built-in Partial, Required, Pick, and Record utilities are all mapped types (see TypeScript Mapped Types).

Remapping Keys with as

TypeScript 4.1 introduced key remapping via the as clause, enabling type-safe property renaming:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
---;

This transforms { name: string } into { getName: () => string }. Key remapping is used in code-generation tools and API client libraries to create typed accessors automatically.

Advanced Patterns

Generic Classes

Classes can be generic, allowing type-safe instances with varied payloads:

class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
---

The Stack<number> and Stack<string> instances are completely type-safe — pushing a number into a Stack<string> is a compile-time error.

Generic Constraints with Multiple Type Parameters

Functions with multiple generic parameters can relate their types:

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
---

For more complex constraints, one parameter can depend on another:

function merge<T, U extends T>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
---

Factory Functions with Generics

Factories that construct instances based on type parameters are common in dependency injection frameworks:

function create<T>(ctor: new (...args: any[]) => T, ...args: any[]): T {
  return new ctor(...args);
---

Generic Type Aliases

Type aliases accept type parameters, enabling reusable type transformations:

type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
---;

Performance Considerations

Generics are fully erased at compile time — they incur zero runtime overhead. The TypeScript compiler resolves generic type parameters during type checking and removes them from the emitted JavaScript. This means you can use liberal generics without worrying about bundle size or execution speed. However, complex conditional types with deep recursion (e.g., recursive template literal types) can slow down the compiler. The handbook recommends keeping recursive type depths below 50 levels for reasonable compilation times.

FAQ

What is the difference between generics and any?
Generics preserve type information; any disables type checking entirely. A generic function returns a type tied to its input, while a function using any abandons all type safety.

Can I use generics with arrow functions in JSX?
Yes, but you need a trailing comma: <T,> to disambiguate from JSX tags. For example: const identity = <T,>(arg: T): T => arg;.

How do I constrain a generic to an object type?
Use T extends Record<string, any> or T extends object. This ensures T is an object while still being flexible.

What is the default value for a generic parameter?
TypeScript supports generic defaults: function createArray<T = string>(length: number, value: T): T[] { ... }. When inference fails, the default applies.

How do conditional types handle union distribution?
Conditional types distribute over bare type parameters. Wrapping the condition in a tuple [T] extends [U] prevents distribution, treating T as a single unit.

Internal Links

References

Section: TypeScript 1455 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top