Interfaces define object shapes and can be extended with extends or merged via declaration merging — type aliases can represent any type (unions, tuples, primitives) but can't be merged, and the practical rule is to use interface for objects and type for everything else.
Define object shapes with optional (?), readonly, and index signatures. Support extends for composition and implements for class contracts.
Can represent any type — unions, intersections, tuples, primitives, function types. Cannot be declaration-merged.
Same-name interfaces auto-merge their members — enables augmenting built-in types and third-party libraries. Type aliases cannot merge.
Interface extends catches property conflicts at the declaration. Type intersection (&) silently produces never for conflicts, causing subtle bugs.
Use interface for object shapes and public APIs. Use type for unions, tuples, and computed types. Both are structurally typed.
Interfaces and type aliases are the two primary ways to name types in TypeScript. They overlap significantly for object types but have distinct capabilities.
Interfaces define the shape of an object — what properties it has and their types:
interface User {
id: number;
name: string;
email: string;
age?: number; // optional property
readonly createdAt: Date; // can't be reassigned
}Optional properties (?) can be undefined or absent. Readonly properties can be set during creation but not reassigned afterward.
Type aliases create a name for any type — not just objects:
type ID = string | number;
type Point = [number, number];
type Callback = (data: string) => void;
type User = { id: number; name: string; };Type aliases can represent unions, intersections, tuples, primitives, function types, and mapped types. Interfaces can only describe object shapes.
Both can compose types, but the syntax differs:
// Interface extension
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
// Type intersection
type Animal = { name: string; };
type Dog = Animal & { breed: string; };Interface extends checks for conflicts — if Dog tries to override a property from Animal with an incompatible type, TypeScript errors. Type intersections (&) silently create a never type for conflicting properties, which causes hard-to-debug errors later.
Interfaces with the same name in the same scope automatically merge their members:
interface Window { customProp: string; }
// Merges with the built-in Window interfaceThis is how TypeScript's built-in types work — Array, Promise, and DOM types are assembled from multiple interface declarations across different .d.ts files. Module augmentation relies on declaration merging.
Type aliases cannot be merged — redeclaring a type alias with the same name is a compile error.
Classes can implement interfaces, creating a contract that the class must satisfy:
interface Serializable { serialize(): string; }
class User implements Serializable {
serialize() { return JSON.stringify(this); }
}Classes can also implement type aliases (if the type describes an object shape), but interface implements is the conventional pattern.
Both interfaces and types support index signatures for dynamic property names:
interface Dictionary { [key: string]: string; }
interface Config { [key: string]: unknown; port: number; } // port must be compatibleThe TypeScript community has converged on a practical rule:
For object types specifically, both work. The TypeScript compiler internally optimizes interface checking slightly better than type alias objects, but the difference is negligible in practice. The real distinction is declaration merging (interface only) and ability to represent non-object types (type alias only).
Interfaces define object shapes with extends for composition and declaration merging for augmentation. Type aliases can represent any type (unions, tuples, primitives). Interface extends catches conflicts; type intersections can silently create never types. Use interface for objects and public APIs; use type for unions, tuples, and computed types. Both are structurally typed — a value matches if it has the right shape, regardless of which name was used.
Fun Fact
The interface vs type debate is one of the longest-running discussions in the TypeScript community. The TypeScript handbook originally recommended interfaces for everything, then switched to recommending type aliases, then settled on 'use interface for objects, type for other things.' The TypeScript team internally uses a mix of both — there's no single correct answer.