Skip to content
TypeScript ts generics 4 min read

Generic Interfaces

A generic interface describes a family of shapes rather than a single fixed one. By adding a type parameter to an interface, you let callers plug in the element type — the type of items in a container, the payload of an API response, or the value of a cache. This is how the standard library types things like Array<T> and Promise<T>, and it is the foundation of every reusable data structure you will write. This page covers declaring generic interfaces, generic methods within them, generic type aliases, and how constraints and defaults fit in.

Declaring a generic interface

Place the type parameter in angle brackets after the interface name, then use it anywhere a type is expected inside the body. A classic example is a generic API response wrapper whose data field varies per endpoint.

interface ApiResponse<T> {
  data: T;
  status: number;
  ok: boolean;
}

interface User {
  id: number;
  name: string;
}

const res: ApiResponse<User> = {
  data: { id: 1, name: "Ada" },
  status: 200,
  ok: true,
};

res.data.name; // string — fully typed

When you write ApiResponse<User>, TypeScript substitutes User for T everywhere it appears, so res.data is a User. The same interface serves every payload type without duplication.

Generic containers and data structures

Generic interfaces shine when modeling collections. Here a simple stack interface keeps the element type consistent across push and pop, so you cannot accidentally mix types.

interface Stack<T> {
  items: T[];
  push(item: T): void;
  pop(): T | undefined;
  peek(): T | undefined;
}

const numberStack: Stack<number> = {
  items: [],
  push(item) {
    this.items.push(item);
  },
  pop() {
    return this.items.pop();
  },
  peek() {
    return this.items.at(-1);
  },
};

numberStack.push(10);
// numberStack.push("oops"); // ❌ Error: Argument of type 'string' is not assignable to 'number'

Because T is number for numberStack, every method enforces that type. A Stack<string> built from the same interface would enforce strings instead.

Generic methods inside interfaces

A type parameter can belong to an individual method rather than the whole interface. This is useful when only one operation needs to be generic — the method introduces its own parameter, resolved per call.

interface Mapper {
  // `U` is scoped to this method, not the interface
  transform<U>(value: string, fn: (input: string) => U): U;
}

const mapper: Mapper = {
  transform(value, fn) {
    return fn(value);
  },
};

const length = mapper.transform("hello", (s) => s.length); // number
const upper = mapper.transform("hello", (s) => s.toUpperCase()); // string

Here Mapper itself is not generic, but transform is. Each call resolves U independently from the function you pass.

Generic type aliases

A type alias can be generic too, and it can express things interfaces cannot — unions, intersections, mapped, and conditional types. A common pattern is a generic Result type for representing success or failure without exceptions.

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseJSON<T>(input: string): Result<T> {
  try {
    return { ok: true, value: JSON.parse(input) as T };
  } catch (err) {
    return { ok: false, error: err as Error };
  }
}

const parsed = parseJSON<User>('{"id":1,"name":"Ada"}');
if (parsed.ok) {
  parsed.value.name; // narrowed to User
}

Use a generic interface for plain object and method shapes that may be extended or implemented; use a generic type alias when you need unions, intersections, or conditional logic.

Constraints and defaults

Generic interfaces accept the same extends constraints and = Default defaults as generic functions. A constraint restricts which types are allowed; a default supplies a fallback when no argument is given.

interface Collection<T extends { id: number } = { id: number }> {
  byId: Map<number, T>;
  all(): T[];
}

// Constraint enforced: every element must have a numeric `id`
type UserCollection = Collection<User>;

// Default used: equivalent to Collection<{ id: number }>
type AnyCollection = Collection;

Interface vs type for generics

FeatureGeneric interfaceGeneric type alias
Object & method shapesYesYes
Declaration mergingYesNo
extends another typeYes (interface A<T> extends B<T>)Via & intersection
Unions / conditionals / mappedNoYes
Default type parametersYesYes

Best Practices

  • Reach for a generic interface whenever a shape’s field or method types vary by use.
  • Keep the type parameter focused on what actually changes; do not parameterize fixed fields.
  • Scope a type parameter to a single method when only that method is generic.
  • Use generic type aliases for unions and conditional shapes like Result<T, E>.
  • Add extends constraints so the interface only accepts compatible element types.
  • Provide a default type parameter when there is a sensible fallback element type.
  • Name parameters descriptively (TItem, TPayload) in interfaces with several of them.
Last updated June 29, 2026
Was this helpful?