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.

6 min read

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:

typescripttypescript
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:

typescripttypescript
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.

typescripttypescript
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:

typescripttypescript
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 factorInterface-level typeMember-level type
Type is shared across multiple membersUse thisDoes not work
Caller should lock in one type for the objectUse thisDoes not apply
Each call might use a different typeOverly restrictiveUse this
Want to write the interface with a type argument at declarationUse thisCannot 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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Should I put the type parameter on the interface or on individual members?

Put the type parameter on the interface when the same type is used across multiple members and the caller should choose it. Put it on individual members when each method or property might need a different type and the choice belongs to the caller of that specific member.

Can a generic interface have a default type parameter?

Yes. You can write `interface Container<T = string> { value: T }` so that `Container` without an explicit type argument defaults to `Container<string>`.

Can I use generic interfaces with classes?

Yes. A class can implement a generic interface, and the class itself can be generic or provide a concrete type: `class StringBox implements Box<string> {}`.

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.