TypeScript Generic Interfaces
A generic interface is an interface whose shape depends on one or more type parameters that are filled in later, at the point of use. Instead of writing a separate interface for every data type you work with, you write the shape once with a placeholder type, and TypeScript substitutes the real type when you use it. Generic interfaces are the backbone of reusable, type-safe APIs — they show up constantly in libraries, framework typings, and your own data structures like caches, stores, and API response wrappers.
Overview / How it works
A normal interface locks in concrete types: interface StringBox { value: string; } only ever works with strings. If you needed the same shape for numbers, booleans, or custom objects, you’d have to duplicate the interface for each one. A generic interface solves this by parameterizing the type: interface Box<T> { value: T; } describes a box holding some type T, and T is decided when the interface is actually used — Box<string>, Box<number>, Box<User>, and so on.
Under the hood, TypeScript treats a generic interface as a template. When you write Box<string>, the compiler substitutes string for every occurrence of T inside the interface body and checks your code against that specialized shape. This happens entirely at compile time: TypeScript uses structural typing, so any object whose properties happen to match the substituted shape satisfies the interface — there’s no need to explicitly declare “this object implements Box<string>” for object literals, only classes use the implements keyword. And critically, since TypeScript’s type system is fully erased when compiling to JavaScript, none of this generic machinery exists at runtime. The compiled JavaScript is just plain objects and functions; the type parameter T leaves no trace.
Generic interfaces can describe more than plain data objects. Because interfaces can describe object shapes, function call signatures, constructor signatures, and index signatures, a generic interface can describe a generic function type, a generic class’s public API, or a generic collection — all using the same <T> syntax.
Syntax
The general form declares one or more type parameters in angle brackets right after the interface name, and then uses those parameters anywhere inside the interface body:
interface Container<T> {
property: T;
method(param: T): T;
}
interface Container<T>— declares the interface and its type parameter, conventionally namedT(orK/Vfor key/value pairs,Ufor a second parameter).property: T— a member whose type is exactly whateverTis instantiated with.method(param: T): T— a method signature can useTfor parameters and return types just like any other type.- Multiple parameters are comma-separated:
interface Dictionary<K, V> { ... }. - A parameter can have a default (
interface ApiResponse<T = unknown>), used when the caller omits a type argument. - A parameter can be constrained with
extends(interface Keyed<T extends { id: string }>), restricting what types are allowed.
An interface can also describe a generic callable value — a function shape — by giving it a call signature instead of named properties:
interface Transformer<T, U> {
(input: T): U;
}
const stringify: Transformer<number, string> = (input) => `Value: ${input}`;
console.log(stringify(42));
Output:
Value: 42
Here Transformer<number, string> describes any function that takes a number and returns a string; the arrow function assigned to stringify is checked against that exact signature.
Examples
Example 1: A basic generic container
interface Box<T> {
value: T;
getValue(): T;
}
class NumberBox implements Box<number> {
constructor(public value: number) {}
getValue(): number {
return this.value;
}
}
const box: Box<string> = {
value: "hello",
getValue() {
return "hello";
},
};
console.log(box.getValue());
const numBox = new NumberBox(42);
console.log(numBox.getValue());
Output:
hello
42
The same Box<T> interface is reused twice: once instantiated as Box<string> for a plain object, and once implemented by a class as Box<number>. TypeScript checks each usage against its own specialized version of the interface — the object literal must have a string value, the class must have a number value — without you writing two separate interfaces.
Example 2: Two type parameters (a key/value store)
interface Dictionary<K, V> {
get(key: K): V | undefined;
set(key: K, value: V): void;
has(key: K): boolean;
}
class MapDictionary<K, V> implements Dictionary<K, V> {
private store = new Map<K, V>();
get(key: K): V | undefined {
return this.store.get(key);
}
set(key: K, value: V): void {
this.store.set(key, value);
}
has(key: K): boolean {
return this.store.has(key);
}
}
const scores: Dictionary<string, number> = new MapDictionary<string, number>();
scores.set("alice", 95);
scores.set("bob", 87);
console.log(scores.get("alice"));
console.log(scores.has("carol"));
Output:
95
false
Dictionary<K, V> takes two type parameters, one for the key and one for the value. MapDictionary is itself generic and implements Dictionary<K, V> by forwarding to a native Map<K, V>. When we declare scores: Dictionary<string, number>, every method on scores is now locked to string keys and number values — calling scores.set(1, "x") would be a compile error.
Example 3: Extending a generic interface (realistic API shape)
interface ApiResponse<T = unknown> {
data: T;
status: number;
timestamp: Date;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
page: number;
pageSize: number;
total: number;
}
function fetchUsers(): PaginatedResponse<{ id: number; name: string }> {
return {
data: [
{ id: 1, name: "Ada" },
{ id: 2, name: "Grace" },
],
status: 200,
timestamp: new Date(0),
page: 1,
pageSize: 2,
total: 2,
};
}
const result = fetchUsers();
console.log(result.data.map((u) => u.name).join(", "));
console.log(result.status, result.page, result.total);
Output:
Ada, Grace
200 1 2
ApiResponse<T = unknown> gives T a default, so ApiResponse alone is legal and defaults to unknown. PaginatedResponse<T> extends ApiResponse<T[]>, re-using and specializing the base shape so data becomes an array of T, while adding pagination fields. This is exactly the pattern real API client libraries use to describe endpoint responses without repeating boilerplate.
Under the hood
When the compiler sees PaginatedResponse<{ id: number; name: string }>, it substitutes that object type for every T in both PaginatedResponse and the ApiResponse<T[]> it extends, producing one fully expanded shape: { data: {id:number;name:string}[]; status: number; timestamp: Date; page: number; pageSize: number; total: number; }. Every property access and function return is checked against that expanded shape, not against the generic template. Because TypeScript is structurally typed, the object literal returned by fetchUsers just needs to have those properties with compatible types — it never needs an explicit annotation saying “this implements PaginatedResponse”.
Once type-checking passes, tsc erases every trace of the generic interface. The compiled JavaScript for Example 3 contains only the object literal, the array, and the two console.log calls — there is no ApiResponse, no PaginatedResponse, and no T anywhere in the output. Generic interfaces exist purely to catch mistakes before your code ever runs; they cost nothing at runtime and cannot be inspected via typeof or reflection.
Common Mistakes
Mistake 1: Forgetting the type argument
A generic interface without a default type parameter requires a type argument wherever it’s used as an annotation:
interface Box<T> {
value: T;
}
let box: Box; // Error: Generic type 'Box<T>' requires 1 type argument(s).
tsc reports Generic type 'Box<T>' requires 1 type argument(s) because T has no default and nothing was supplied. The fix is to always supply the type argument (or give the interface a default so it can be omitted):
interface Box<T> {
value: T;
}
let box: Box<string>;
box = { value: "hello" };
console.log(box.value);
Output:
hello
Mistake 2: A class claims to implement a generic interface but is missing a member
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Comparable<Money> {
constructor(public amount: number) {}
// Error: Class 'Money' incorrectly implements interface 'Comparable<Money>'.
// Property 'compareTo' is missing in type 'Money'.
}
Because implements asks TypeScript to verify the class shape against the interface, leaving out compareTo is a compile error, not a runtime surprise. Adding the missing method fixes it:
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Comparable<Money> {
constructor(public amount: number) {}
compareTo(other: Money): number {
return this.amount - other.amount;
}
}
const a = new Money(10);
const b = new Money(25);
console.log(a.compareTo(b));
Output:
-15
Best Practices
- Name type parameters meaningfully once an interface has more than one:
K/Vfor key/value,T/Ufor input/output, rather than always defaulting toT,T2,T3. - Give a type parameter a sensible default (
<T = unknown>) when the interface is commonly used without needing to specialize it, so callers aren’t forced to write out a type argument every time. - Prefer
extendsto compose generic interfaces (likePaginatedResponse<T> extends ApiResponse<T[]>) instead of copy-pasting shared fields into multiple interfaces. - Constrain type parameters with
extends(e.g.<T extends { id: string }>) when the interface’s methods genuinely need to rely on some property ofT— this documents the requirement and gives better error messages than leavingTunconstrained. - Avoid reaching for
anyas a substitute for a real type parameter;anythrows away the type safety generics exist to provide. - Remember types vanish at runtime — never rely on a generic interface to perform runtime checks or validation; use runtime guards or a validation library for that.
Practice Exercises
- Exercise 1: Write a generic interface
Stack<T>with methodspush(item: T): void,pop(): T | undefined, andpeek(): T | undefined. Then write a class that implementsStack<string>. - Exercise 2: Write a generic interface
Repository<T>withfindById(id: number): T | undefinedandsave(item: T): void. Create aninterface User { id: number; name: string; }and a classUserRepository implements Repository<User>backed by an array. - Exercise 3: Write a generic interface
Result<T, E = string>that can represent either{ ok: true; value: T }or{ ok: false; error: E }(hint: you’ll likely want a union type rather than a single object shape). Write a function that returns aResult<number>and logs the value or error depending on which branch it got.
Summary
- A generic interface uses one or more type parameters, declared in
<...>after the interface name, as placeholders that are filled in at the point of use. - The same generic interface can be instantiated with different concrete types (
Box<string>,Box<number>) instead of duplicating the shape per type. - Type parameters can have defaults (
<T = unknown>) and constraints (<T extends ...>), and generic interfaces canextendother generic interfaces. - An interface’s call signature can describe a generic function type, not just object shapes.
- TypeScript checks generic interfaces structurally at compile time and fully erases them — the compiled JavaScript has no trace of type parameters or interfaces at all.
