TypeScript Interface vs Type Cheat Sheet
Side-by-side comparison of interface and type alias syntax, capabilities, and trade-offs -- with a quick decision table.
Basic Syntax Comparison
Both can define object shapes. The syntax differs, but the result is often identical.
interface User {
id: number;
name: string;
email: string;
}
const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };type User = {
id: number;
name: string;
email: string;
};
const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };For plain object shapes, these produce the same result. The choice is a style preference.
When to Use Interface
Interface wins when you need `extends` inheritance or declaration merging.
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
}
// Dog has: name, age, breed
const dog: Dog = { name: 'Rex', age: 3, breed: 'Labrador' };interface HasId { id: string }
interface HasTimestamps { createdAt: Date; updatedAt: Date }
interface Article extends HasId, HasTimestamps {
title: string;
body: string;
}
// Article has: id, createdAt, updatedAt, title, body// First declaration
interface Window {
title: string;
}
// Second declaration -- merges with the first
interface Window {
appVersion: string;
}
// Window now has both: title and appVersion
const w: Window = { title: 'My App', appVersion: '1.0.0' };Declaration merging is how libraries add properties to global types. Type aliases can't do this.
When to Use Type
Type aliases handle unions, primitives, tuples, and mapped types -- things interfaces can't express.
type Status = 'pending' | 'active' | 'archived';
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: string };type ID = string | number;
type Coordinate = [x: number, y: number];
type Callback = (event: Event) => void;
// None of these can be expressed with interfacetype Flags<T> = {
[K in keyof T]: boolean;
};
interface Features {
darkMode: string;
notifications: string;
analytics: string;
}
type FeatureFlags = Flags<Features>;
// => { darkMode: boolean; notifications: boolean; analytics: boolean }Mapped types use the `in` keyword to iterate over keys. This only works with type aliases.
Object Shapes (Both Work)
For plain object types, interface and type are interchangeable. Pick a convention and stick with it.
// Interface version
interface ConfigI {
readonly apiUrl: string;
timeout?: number;
debug?: boolean;
}
// Type version -- identical behavior
type ConfigT = {
readonly apiUrl: string;
timeout?: number;
debug?: boolean;
};// Interface
interface StringMap {
[key: string]: string;
}
// Type
type StringMapT = {
[key: string]: string;
};
// Both accept the same values
const headers: StringMap = { 'Content-Type': 'application/json' };// Interface
interface Formatter {
format(value: string): string;
}
// Type
type FormatterT = {
format(value: string): string;
};
// Both accept the same implementation
const upper: Formatter = { format: (v) => v.toUpperCase() };Extending and Composing
Interfaces use `extends`. Types use intersection (`&`). Both combine types, but error messages differ.
interface Base { id: number }
interface WithName extends Base { name: string }
const item: WithName = { id: 1, name: 'Alice' };type Base = { id: number };
type WithName = Base & { name: string };
const item: WithName = { id: 1, name: 'Alice' };// Type extending an interface
interface Animal { name: string }
type Pet = Animal & { owner: string };
// Interface extending a type
type HasId = { id: number };
interface User extends HasId { name: string }Interfaces and types interoperate freely. An interface can extend a type alias and vice versa.
// Interface extends -- conflicts are caught:
interface A { x: number }
// interface B extends A { x: string }
// => Error: 'x' in B is incompatible with 'x' in A
// Type intersection -- conflicts produce never:
type C = { x: number } & { x: string };
// C.x is type: never (number & string = nothing)Interface extends gives a clear error on property conflicts. Intersection silently produces `never` for the conflicting property -- harder to debug.
Declaration Merging
Interfaces with the same name merge their declarations automatically. Type aliases can't do this.
// In your app code, extend Express Request:
declare module 'express' {
interface Request {
userId?: string;
sessionId?: string;
}
}
// Now req.userId is available in handlers
// This is the primary real-world use of declaration merging// Add a custom property to the global Window
declare global {
interface Window {
analytics: {
track(event: string, data?: Record<string, unknown>): void;
};
}
}
// Now window.analytics.track('pageview') type-checkstype Config = { host: string };
// type Config = { port: number };
// => Error: Duplicate identifier 'Config'
// If you need merging, use interface instead:
interface Config { host: string }
interface Config { port: number }
// Config now has both host and portTS 5 Behavior Changes
TypeScript 5 narrowed some differences between interfaces and types.
// TS 5.0 improved performance for intersection types.
// Large intersections that caused slowdowns in TS 4.x are now faster.
// Before (TS 4.x): complex intersections could cause slow type-checking
type BigIntersection = A & B & C & D & E & F; // could stall
// After (TS 5.0+): same type checks faster due to
// internal deferred type evaluation// TS 5.x produces clearer error messages for both:
interface UserI { name: string; age: number }
type UserT = { name: string; age: number };
// Both now show structured diffs on mismatch:
// const user: UserI = { name: 'Alice', age: 'thirty' };
// ~~~~~~~~~~~
// Type 'string' is not assignable to type 'number'.TS 5 made error messages more consistent between interface and type alias mismatches. This was a historical difference that no longer matters.
Quick Decision Table
Use this table when you're unsure which to pick.
// Use INTERFACE when:
// - Defining an object shape you might extend later
// - You need declaration merging (augmenting libraries)
// - You want clearer error messages on property conflicts
// - Your team convention says "interface for objects"
// Use TYPE when:
// - Defining a union type (A | B)
// - Defining a tuple type ([string, number])
// - Defining a primitive alias (type ID = string)
// - Using mapped types or conditional types
// - Composing types with intersection (&)
// - Your team convention says "type for everything"
// Use EITHER when:
// - Defining a plain object shape (both work identically)
// - The decision doesn't matter (be consistent, pick one)// Convention A: "interface for objects, type for everything else"
interface User { name: string } // object shape
type Status = 'active' | 'inactive'; // union
type Pair = [string, number]; // tuple
// Convention B: "type for everything"
type User = { name: string };
type Status = 'active' | 'inactive';
type Pair = [string, number];
// Both are valid. The TypeScript team uses Convention A internally.Consistency within a codebase matters more than the specific choice. Use ESLint's consistent-type-definitions rule to enforce your convention.
Can you write this from memory?
Create Admin interface extending User with role: string