TypeScript Interfaces vs Type Aliases: The Real Differences
What's actually different between interface and type in TypeScript, when the choice matters, when it doesn't, and what the community convention is.
This question comes up in every TypeScript codebase eventually. Someone writes an interface, someone else writes a type, and then there's a code review comment asking why. The honest answer is that 95% of the time, either one works fine. But there are real differences, and knowing them saves you from the 5% where it actually matters.
The Quick Version
Both can describe the shape of an object:
interface User {
id: number;
name: string;
email: string;
}
type User = {
id: number;
name: string;
email: string;
};
For this case, they're interchangeable. You can use either with classes, function parameters, generics -- all of it works the same.
Where They Diverge
Declaration Merging (Interfaces Only)
If you declare the same interface twice, TypeScript merges them:
interface Window {
myCustomProp: string;
}
// This merges with the built-in Window interface
// Now window.myCustomProp is typed
Type aliases can't do this. Declaring the same type name twice is an error:
type Window = { myCustomProp: string }; // ERROR: Duplicate identifier
Declaration merging matters for library authors who want consumers to extend types. It's why most of the built-in DOM types and library definitions use interfaces. But in application code, you almost never want two declarations of the same name merging silently -- that's more confusing than helpful.
Unions and Intersections (Type Aliases)
Type aliases can express things interfaces can't:
type Status = 'loading' | 'success' | 'error';
type StringOrNumber = string | number;
type Result<T> = { success: true; data: T } | { success: false; error: string };
An interface can't be a union. It has to describe a single object shape. If you need a union, discriminated union, or any non-object type, you need type.
Extending vs Intersection
Interfaces extend other interfaces:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
Type aliases use intersection:
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
Both produce the same result. There's a subtle difference though: extends checks for conflicts at declaration time, while & silently creates never types for conflicting properties. Consider:
interface A {
x: string;
}
// This is an error -- x can't be both string and number
interface B extends A {
x: number;
}
// This compiles but x becomes 'string & number' which is 'never'
type C = A & { x: number };
The interface version catches the mistake immediately. The intersection silently creates an unusable type. Point to interfaces here.
Mapped Types (Type Aliases Only)
You can't use mapped types with interfaces:
type Flags = {
[K in 'darkMode' | 'notifications' | 'beta']: boolean;
};
// { darkMode: boolean; notifications: boolean; beta: boolean }
No interface equivalent exists. Utility types like Pick, Omit, Partial, Record -- they all produce type aliases, not interfaces.
Computed Properties
Similarly, type aliases support template literal types and other computed patterns:
type EventName = 'click' | 'scroll' | 'resize';
type EventHandlers = {
[K in EventName as on${Capitalize<K>}]: () => void;
};
// { onClick: () => void; onScroll: () => void; onResize: () => void }
Interfaces can't do this.
Performance
The TypeScript team has noted that interfaces can be slightly faster for type checking in large codebases because they create a named reference that gets cached, while complex intersections need to be flattened every time. In practice, you won't notice this unless your project has thousands of types. But it's worth knowing if you're maintaining a large shared library.
When the Choice Actually Matters
| Scenario | Use |
|---|---|
| Union types, discriminated unions | type (interfaces can't) |
| Mapped types, utility types | type (interfaces can't) |
Primitive aliases (type ID = string) | type (interfaces can't) |
| Library definitions that consumers extend | interface (declaration merging) |
| Class implements | Either (both work) |
| Object shapes in application code | Either (convention preference) |
| Catching property conflicts early | interface extends (stricter checking) |
The Community Convention
Most teams settle on this pattern:
- Interfaces for object shapes -- things with properties and methods
- Type aliases for everything else -- unions, intersections, utility types, primitives, tuples
Some teams just use type for everything. That works too. The worst thing you can do is mix randomly with no rationale -- pick a convention and stick with it.
My Honest Take
If you're starting a new project, use interfaces for your data models and type aliases for everything else. If you're joining an existing project, follow whatever they're already doing. If someone argues passionately about this in a code review, they're spending energy on the wrong thing.
The real TypeScript skill isn't choosing between interface and type -- it's learning the type system deeply enough to model your domain correctly. Generics, discriminated unions, narrowing, conditional types -- those are the things worth investing time in.
Practice both patterns with interactive TypeScript exercises on CodeUp -- the best way to internalize this stuff is to write it yourself and see what the compiler tells you.