TypeScript type vs interface: When to Use Each
Practice4 minInnovialabs editorial team
In short
For object shapes, type and interface are almost interchangeable. Use interface when extends and declaration merging matter, and type for unions, tuples and computed types. Most importantly, agree on one team rule.
The short answer
For a plain object there is almost no difference:
type UserT = { id: number; name: string };
interface UserI { id: number; name: string }
Both work the same in functions, classes and component props. The differences show up in four places: declaration merging, unions and other non-object types, extends vs intersections and error messages.
Difference 1: declaration merging
Two interface declarations with the same name merge automatically:
interface Config { apiUrl: string }
interface Config { timeout: number }
const c: Config = { apiUrl: "/api", timeout: 5000 }; // both fields required
You cannot do this with type — a second declaration fails with Duplicate identifier.
When it helps: extending third-party types — for example, adding a field to the global Window or to a library's types. When it hurts: an accidental name clash across files silently changes the type.
Difference 2: unions and non-object types
An interface only describes an object shape. A type can do everything else:
type Status = "idle" | "loading" | "error";
type Id = string | number;
type Point = [number, number];
type Handler = (event: MouseEvent) => void;
type ReadonlyUser = Readonly<UserT>;
type Result =
| { ok: true; data: string }
| { ok: false; error: string };
Unions, tuples, mapped and conditional types belong to type.
Difference 3: extends vs intersections
Inheritance can be written two ways:
interface Base { id: number }
interface Admin extends Base { role: "admin" }
type BaseT = { id: number };
type AdminT = BaseT & { role: "admin" };
The difference shows up on a property conflict:
interface A { value: string }
interface B extends A { value: number }
// Immediate error: Interface 'B' incorrectly extends interface 'A'
type C = { value: string } & { value: number };
// No error, but value is never — the problem surfaces later
extends checks compatibility at declaration time, while the & intersection silently produces an impossible type. The TypeScript docs also note that the compiler handles interface extends hierarchies more efficiently than large intersections.
Difference 4: error messages
In hovers and errors, an interface is usually shown by name, while a complex type built from intersections and utilities may expand into a long structure. In a large project this noticeably affects error readability. Not a fundamental difference, but a nice point for interface on objects.
What both can do
- Describe objects, methods, optional and
readonlyfields. - Be used in a class
implementsclause (as long as thetypeis an object, not a union). - An
interfacecan extend atype, and atypecan intersect aninterface. - Support generics:
interface Box<T> { value: T }andtype Box<T> = { value: T }.
Comparison table
| Capability | type | interface |
|---|---|---|
| Object shape | Yes | Yes |
| Unions, tuples, primitives | Yes | No |
| Mapped / conditional types | Yes | No |
| Declaration merging | No | Yes |
| Conflict check on inheritance | No (& gives never) | Yes (extends) |
| Name shown in errors | Not always | Usually |
A simple team rule
A convention that is easy to follow:
interfacefor object shapes: data models, component props, API contracts, public library types.typefor unions, tuples, functions and anything composed from utilities (Pick,Omit,Partial).- Inheritance through
extendsrather than&where possible. - Declaration merging only on purpose, to extend external types.
The alternative "type everywhere, interface only to extend external types" is fine too. What matters is picking one and enforcing it with a linter: typescript-eslint has the consistent-type-definitions rule.