Generic Interfaces in TypeScript
A generic interface defines a reusable shape where member types are filled in later. Learn how to declare generic interfaces and choose between interface-level and member-level type parameters.
TypeScript generic interfaces are interfaces that have one or more type parameters. Instead of fixing every property and method to a concrete type, you leave some types as variables that the caller fills in. This lets you define a shape once and reuse it with different types while keeping full type safety.
Here is a simple generic interface:
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };The type parameter T sits on the interface itself. When you write Box<number>, TypeScript replaces every occurrence of T inside the interface with number. The value property of the first box is a number, and the value property of the second box is a string.
Interface-Level vs Member-Level Type Parameters
There are two places you can put a type parameter on an interface: on the interface itself or on individual call signatures. The choice changes how the interface behaves.
Interface-level type parameter:
interface Repository<T> {
getById(id: string): T;
save(item: T): void;
}The type parameter is shared across both methods. A repository typed with a User entity always gets and saves User objects. The type is consistent across the whole object.
const userRepo: Repository<{ name: string }> = {
getById(id) {
return { name: "Ada" };
},
save(item) {
console.log(item.name);
},
};A member-level type parameter works differently. Instead of fixing the type for the whole interface, it lets each call to the same method choose its own type:
interface GenericFn {
<T>(items: T[]): T | undefined;
}
const first: GenericFn = (items) => items[0];
const a = first([1, 2, 3]);
const b = first(["x", "y"]);Here the type parameter belongs to the call signature, not the interface, so you never write a type argument when declaring GenericFn. TypeScript infers a fresh type on every call.
The first call infers number, so a may be number or undefined. The second call infers string, so b may be string or undefined.
Which One to Choose
| Decision factor | Interface-level type | Member-level type |
|---|---|---|
| Type is shared across multiple members | Use this | Does not work |
| Caller should lock in one type for the object | Use this | Does not apply |
| Each call might use a different type | Overly restrictive | Use this |
| Want to write the interface with a type argument at declaration | Use this | Cannot write |
A good rule: if the type describes the identity of the object, put it on the interface. If it describes one operation the object can perform, put it on the member.
Generic Interfaces with Multiple Type Parameters
An interface can have more than one type parameter, just like generic functions:
interface KeyValuePair<K, V> {
key: K;
value: V;
}
const entry: KeyValuePair<string, number> = {
key: "age",
value: 30,
};This pattern is common for dictionaries, caches, and any structure where two types are related but independent.
Generic Interfaces and Type Aliases
You can also write generic types with the type keyword. The choice between interface and type for generics follows the same rules as non-generic types:
type Pair<T, U> = {
first: T;
second: U;
};
const p: Pair<string, boolean> = { first: "done", second: true };For most generic object shapes, the interface keyword and type keyword are interchangeable. Use interface when you want declaration merging or plan to extend the shape, and use type when you want union, intersection, or mapped types.
For a detailed comparison, see interface vs type in TypeScript.
Generic Interfaces with Classes
A class can implement a generic interface. When the class itself is generic, the caller chooses the type:
interface Stack<T> {
push(item: T): void;
pop(): T | undefined;
}
class ArrayStack<T> implements Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const numberStack = new ArrayStack<number>();
numberStack.push(10);A class can also implement the interface with a concrete type instead of staying generic. That locks every instance of the class to that one type, so the class no longer needs its own type parameter:
class StringStack implements Stack<string> {
private items: string[] = [];
push(item: string): void { this.items.push(item); }
pop(): string | undefined { return this.items.pop(); }
}The first approach lets the caller choose the type. The second approach locks the class to a specific type.
A Practical Pattern: Generic Repository
A common use for generic interfaces is defining data access patterns:
interface Repository<T> {
findAll(): T[];
findById(id: string): T | undefined;
create(item: Omit<T, "id">): T;
update(id: string, item: Partial<T>): T | undefined;
delete(id: string): boolean;
}Any entity type can reuse this interface. A repository typed with a User entity guarantees that findAll returns User arrays and create accepts a User without the id field.
A repository typed with a Product entity does the same for products. You write the shape once and get type safety for every entity.
The Omit and Partial utility types used here are covered in utility types and mapped types.
Common Mistakes
Putting the type parameter on the interface when each call should be independent:
interface Logger<T> {
log(message: T): void;
}
const logger: Logger<string> = {
log(message) {
console.log(message);
},
};Now logger.log only accepts strings. If you wanted to log anything, the type parameter should have been on the call signature instead.
Forgetting to provide a type argument when the interface has no default:
interface Wrapper<T> {
data: T;
}
const w: Wrapper = { data: "hello" };This is an error. TypeScript needs to know what T is. Either write Wrapper<string> or add a default type parameter.
For more on default type parameters, see default generic type parameters.
Rune AI
Key Insights
- A generic interface has one or more type parameters in angle brackets after the interface name.
- Type parameters on the interface itself make the whole object generic and consistent across all members.
- Type parameters on individual call signatures let each use pick a different type.
- Generic interfaces work with classes, type aliases, and object literals the same way.
- Type parameters are erased at compile time and do not affect the emitted JavaScript.
Frequently Asked Questions
Should I put the type parameter on the interface or on individual members?
Can a generic interface have a default type parameter?
Can I use generic interfaces with classes?
Conclusion
Generic interfaces let you define reusable shapes where the exact types are filled in by the caller. When the type parameter sits on the interface itself, the whole object is locked to one type. When it sits on individual members, each use can pick a different type. Choose based on whether the type binds the entire object together or just one operation.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.