Interfaces vs Type Aliases
Same job 80% of the time. The 20% where the answer is forced.
Interfaces vs Type Aliases
The most asked TypeScript question after "what's any" is "interface or type?" The answer is "it depends on three forced cases — and beyond that, taste." Here's how to know which case you're in.
What they have in common
For ordinary object shapes, both work and produce identical types:
interface User { id: number; name: string }
type User = { id: number; name: string };
Both can:
- describe object shapes
- be extended (
extendsfor interface,&intersection for type) - include optional properties (
?), readonly modifiers, index signatures, and call signatures - be implemented by classes via
implements
For 80% of code, swapping one for the other compiles cleanly. The 20% is what matters.
Three forced cases for type
1. Unions
interface is for object shapes. A union of two object shapes is not an object shape:
type Result = "ok" | "error";
type Shape = Circle | Square | Triangle;
// interface Result = ... ❌ syntax error
The same goes for null | string, 1 | 2 | 3, every literal union — type only.
2. Primitives
interface UserId = string is not legal syntax. If you want a name for a primitive, you need an alias:
type UserId = string;
type Pixels = number;
type IsoDate = `${number}-${number}-${number}`; // template-literal type
3. Tuples
Tuples are positional types, not property bags:
type Coord = [number, number];
type Pair<A, B> = [A, B];
interface can mimic this with index signatures, but the result is uglier and loses positional inference.
4. Mapped & conditional types
Anything computed from another type — Partial<T>, Pick<T, K>, T extends U ? X : Y — needs type:
type Optional<T> = { [K in keyof T]?: T[K] };
type Returned<T> = T extends () => infer R ? R : never;
Two forced cases for interface
1. Declaration merging
You can declare an interface twice in the same scope and TypeScript merges the declarations:
interface Window {
myAppConfig: { apiUrl: string };
}
// Combined with the lib.dom.d.ts declaration of Window —
// `window.myAppConfig` is now typed.
This is the only way to extend types defined in third-party libraries (or lib.dom.d.ts) without forking the upstream. If you've ever added a property to globalThis, augmented Express.Request, or extended JQuery, you've used declaration merging — which forces interface.
type Window = { myAppConfig: { apiUrl: string } };
// ❌ Duplicate identifier 'Window'
2. Public library surfaces
By extension, library authors should publish object types as interfaces. Doing so lets consumers extend the type by re-declaring it. If you publish type User = ..., downstream code is locked out — they have to wrap your type to extend it.
What the official guide says
The TypeScript handbook recommends: "Use interface until you need to use features from type." That's the right default. The two reasons:
- Better error messages. TypeScript displays interface names in errors; type aliases get inlined into the offending expression. With deep types this is a real readability difference.
extendsis symmetric and obvious.interface Admin extends Userreads like English.type Admin = User & { role: "admin" }is fine but more abstract.
Common confusions
"Interface is faster to compile than type." Used to be true in some narrow cases. Modern TS has closed the gap; don't optimise on this axis.
"Type can do everything interface can." Almost — except declaration merging. That single feature is non-trivial to lose if your type ever ships in .d.ts.
"Type unions are safer." Neither is safer. They express different things: union types ARE values that can be one of N shapes; an interface IS one shape. Pick the meaning you want.
A decision tree
Is it a union, primitive, tuple, or computed type?
→ type
Will external code extend this via declaration merging?
→ interface
Is it an object shape used internally?
→ either; prefer interface for consistency
What teams usually settle on
A common convention that survives style-guide debates:
interfacefor every object shape. Forces consistency, plays well with tooling, gives nicer error messages.typefor unions, mapped types, function aliases, and anywhere elseinterfacesimply can't go.- ESLint rule
@typescript-eslint/consistent-type-definitionsenforces this.
That keeps the question off the PR review surface — which is mostly the point.
A worked example
// type for the union and computed
type Status = "active" | "archived" | "deleted";
type StatusColors = { [S in Status]: string };
// interface for shapes (incl. extension)
interface User {
id: number;
name: string;
status: Status;
}
interface Admin extends User {
permissions: readonly string[];
}
// type for the function (cleaner reads than interface call-signature)
type Save = (u: User) => Promise<void>;
Three different tools, each used where it's clearly best. That's the whole game.
Tools in the wild
3 tools- serviceTypeScript Playgroundfree tier
Compare interface vs type errors side-by-side instantly.
- library
Enforce a project-wide preference (interface vs type) for object shapes.
- librarytsdfree tier
Test the public type surface of your library — catches accidental breaking changes.