Interfaces vs Type Aliases
TypeScript gives you two ways to name and describe a shape: interfaces and type aliases. They look similar on the surface — both can describe object shapes, both can be used as function parameter types — but they have meaningful differences that affect readability, extensibility, and the error messages you see when things go wrong.
This page walks through every practical difference with real examples, a decision flowchart, and style-guide consensus so you can make the right call every time.
What They Have in Common
Before diving into differences, it helps to see how similar they are for everyday use. Both of these describe the same object shape and are interchangeable in most situations:
// Interface
interface User {
id: number;
name: string;
email: string;
}
// Type alias
type User = {
id: number;
name: string;
email: string;
};
// Both work identically here
function greet(user: User): string {
return `Hello, ${user.name}!`;
}
interface and type produce zero JavaScript output — they are erased at compile time. They exist only to help TypeScript check your code.Declaration Merging (Interfaces Only)
The single biggest behavioral difference: interfaces support declaration merging, type aliases do not.
If you declare two interfaces with the same name, TypeScript merges them into one. This is intentional and heavily used in the TypeScript ecosystem for augmenting third-party types.
// Both declarations merge automatically
interface Window {
myAnalytics: () => void;
}
interface Window {
featureFlags: Record<string, boolean>;
}
// TypeScript sees a single merged type:
// Window has both myAnalytics AND featureFlags
declare const w: Window;
w.myAnalytics();
w.featureFlags['darkMode'];
This is how libraries like @types/node, express, and many others let you augment
built-in types. For example, Express uses merging so you can add req.user to the
Request interface in your own project:
// In your project: src/types/express.d.ts
declare global {
namespace Express {
interface Request {
user?: AuthenticatedUser;
}
}
}
// Now req.user is typed everywhere in your Express app
app.get('/profile', (req, res) => {
const user = req.user; // AuthenticatedUser | undefined
});
Duplicate identifier 'X'.type Config = { debug: boolean };
type Config = { verbose: boolean }; // ERROR: Duplicate identifier 'Config'
Extending: extends vs Intersection (&)
Both interfaces and type aliases support composition, but with different syntax and slightly different semantics.
Interface extends
interface Animal {
name: string;
sound(): string;
}
interface Pet extends Animal {
owner: string;
}
interface ServiceDog extends Pet {
certification: string;
tasks: string[];
}
const buddy: ServiceDog = {
name: 'Buddy',
owner: 'Alice',
certification: 'ADA-2024',
tasks: ['guide', 'alert'],
sound() { return 'woof'; },
};
Type alias intersection (&)
type Animal = {
name: string;
sound(): string;
};
type Pet = Animal & {
owner: string;
};
type ServiceDog = Pet & {
certification: string;
tasks: string[];
};
const buddy: ServiceDog = {
name: 'Buddy',
owner: 'Alice',
certification: 'ADA-2024',
tasks: ['guide', 'alert'],
sound() { return 'woof'; },
};
Key difference: conflicting properties
When the same property appears in both sides of an intersection, TypeScript tries to
intersect their types. For primitive types, intersecting string & number produces
never — a type no value can satisfy. With extends, you get a hard compile error
instead, which is usually more helpful.
// With type intersection — silent never
type A = { id: string };
type B = { id: number };
type C = A & B; // { id: never } — compiles, but unusable
const c: C = { id: 'hello' }; // ERROR only when you try to assign
const c2: C = { id: 42 }; // ERROR only when you try to assign
// With interface extends — immediate error
interface D { id: string }
interface E extends D { id: number } // ERROR: Types of property 'id' are incompatible
interface extends when building class hierarchies or extending third-party types — the error messages appear at the declaration site, not later at the usage site.Union Types (Type Aliases Only)
This is something interfaces simply cannot do. A union type says "this value is one of these types". You can only express unions with a type alias:
// Union of primitives
type Status = 'pending' | 'active' | 'cancelled';
type ID = string | number;
// Union of shapes (discriminated union pattern)
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
case 'triangle':
return 0.5 * shape.base * shape.height;
}
}
// Union of built-in types
type StringOrBuffer = string | Buffer;
type MaybeNull<T> = T | null;
Discriminated unions (also called tagged unions) are one of TypeScript's most powerful patterns for modeling real-world data — API responses, Redux actions, state machines. They are only possible with type aliases.
// API response pattern — interface cannot do this
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string; code: number }
| { status: 'loading' };
function handleResponse(res: ApiResponse<User[]>) {
if (res.status === 'success') {
console.log(res.data); // User[] — TypeScript knows
} else if (res.status === 'error') {
console.error(res.message, res.code); // string, number
}
}
Mapped Types and Conditional Types (Type Aliases Only)
Advanced type transformations require type aliases. These are the building blocks of
TypeScript's utility types (Partial, Required, Readonly, Pick, etc.).
Mapped types
// Roll your own Partial
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Roll your own Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// All string values
type Stringify<T> = {
[K in keyof T]: string;
};
interface Product {
id: number;
name: string;
price: number;
}
type PartialProduct = MyPartial<Product>;
// { id?: number; name?: string; price?: number }
type StringifiedProduct = Stringify<Product>;
// { id: string; name: string; price: string }
Conditional types
// Extract the resolved value of a Promise type Awaited<T> = T extends Promise<infer U> ? U : T; type A = Awaited<Promise<string>>; // string type B = Awaited<number>; // number (not a Promise, passthrough) // Filter array element types type ElementType<T> = T extends (infer U)[] ? U : never; type C = ElementType<string[]>; // string type D = ElementType<number[]>; // number type E = ElementType<boolean>; // never // NonNullable implementation type MyNonNullable<T> = T extends null | undefined ? never : T; type F = MyNonNullable<string | null | undefined>; // string
Tuple and Primitive Aliases (Type Aliases Only)
Type aliases can name any TypeScript type — not just objects. Interfaces can only describe object shapes (and function/constructor signatures).
// Primitive alias
type Milliseconds = number;
type UserID = string;
// Tuple types
type Pair<A, B> = [A, B];
type RGB = [red: number, green: number, blue: number];
type Entry<K, V> = [key: K, value: V];
const color: RGB = [255, 128, 0]; // orange
// Function types
type Predicate<T> = (value: T) => boolean;
type Transformer<A, B> = (input: A) => B;
const isPositive: Predicate<number> = (n) => n > 0;
const double: Transformer<number, number> = (n) => n * 2;
// Template literal types
type EventName = `on${Capitalize<string>}`;
type CSSProperty = `--${string}`;
Performance Considerations
For most projects, the performance difference is negligible. But if you are authoring a large library or working on a codebase with thousands of types, it is worth knowing:
-
Interfaces are cached by the TypeScript compiler. Once an interface is checked, its result is stored and reused. Complex intersections of type aliases are re-evaluated each time they appear, which can slow large projects.
-
The TypeScript team's own performance wiki recommends preferring interfaces over type aliases for object shapes when you do not need union or mapped type features.
-
In practice, you will only notice this on projects with hundreds of complex generic types. Do not over-optimise prematurely — choose the tool that expresses your intent most clearly.
tsc --diagnostics or tsc --extendedDiagnostics to see which files and types are taking longest. Swapping hot intersections for interfaces is one tuning lever.Style Guide Recommendations
Different style guides have reached slightly different conclusions. Here is a summary of the mainstream consensus:
TypeScript team (official)
The TypeScript Handbook says to use interfaces for object shapes when possible, and type aliases when you need unions, tuples, or mapped/conditional types. The compiler error messages for interfaces are generally cleaner.
Google TypeScript Style Guide
Prefer interfaces for defining the shape of objects (records, classes, APIs). Use type aliases for unions, intersections, and utility-type transformations.
Airbnb TypeScript conventions
Airbnb's guide (aligned with @typescript-eslint/consistent-type-definitions) leans
toward interfaces for public API boundaries and exported types, allowing type aliases
for local/internal utility types.
Community consensus (2024 onwards)
The practical consensus that has emerged:
- Interfaces for objects you might extend, implement in a class, or augment from outside.
- Type aliases for everything else — unions, mapped types, conditional types, tuples, primitives, and complex generic utilities.
- Be consistent within a codebase — mixing both randomly is more confusing than picking one default and reserving the other for specific use cases.
Class Compatibility
Both interfaces and type aliases can be used with implements, but there is one caveat
for type aliases:
// Class implementing an interface
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
class UserSession implements Serializable {
private data: Record<string, unknown> = {};
serialize(): string {
return JSON.stringify(this.data);
}
deserialize(data: string): void {
this.data = JSON.parse(data);
}
}
// Class implementing a type alias — works the same
type Loggable = {
log(message: string): void;
};
class ConsoleLogger implements Loggable {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
class Foo implements A | B is a compile error because a class cannot satisfy multiple alternative shapes simultaneously.type AorB = { a: string } | { b: number };
class Foo implements AorB { // ERROR: A class can only implement an object type
a = 'hello';
}
Recursive Types
Both interfaces and type aliases support recursion, which is useful for tree structures, JSON, and nested data.
// Recursive interface
interface TreeNode {
value: number;
left?: TreeNode;
right?: TreeNode;
}
const tree: TreeNode = {
value: 1,
left: { value: 2, left: { value: 4 } },
right: { value: 3 },
};
// Recursive type alias — also fine
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const config: JSONValue = {
name: 'app',
version: 2,
features: ['auth', 'billing'],
settings: { debug: false, timeout: null },
};
Decision Flowchart
Use this decision process when you are unsure which to reach for:
Do you need a union type (A | B)? Use a type alias. Interfaces cannot express unions.
Do you need a mapped type, conditional type, or template literal type? Use a type alias. These are type-alias-only features.
Do you need a tuple type or a primitive alias? Use a type alias.
Will this type be implemented by a class or extended by other interfaces in a public API? Use an interface. Better for declaration merging and class hierarchies.
Are you augmenting a third-party library type (e.g. Express Request, global Window)? You must use an interface. Only interfaces support declaration merging.
Is it a simple object shape for internal use where none of the above apply? Either works. Use whichever your codebase already defaults to for consistency.
Comparison Table
Feature | interface | type alias |
|---|---|---|
Describe object shapes | Yes | Yes |
Extend / compose | extends keyword | & intersection |
Declaration merging | Yes | No |
Union types (A | B) | No | Yes |
Mapped types | No | Yes |
Conditional types | No | Yes |
Tuple types | No | Yes |
Primitive aliases | No | Yes |
Class implements | Yes | Yes (non-union only) |
Recursive types | Yes | Yes |
Compiler caching | Better | Re-evaluated per use |
Error message clarity | Generally cleaner | Can be verbose for intersections |
Common Mistakes to Avoid
Using type aliases everywhere out of habit — you miss out on declaration merging when you need to augment library types.
Using interfaces for everything — you cannot express unions, making discriminated union patterns impossible.
Intersecting conflicting property types with & — produces 'never' silently instead of an error at the declaration.
Using interface to describe function types — works, but type aliases are more readable: type Handler = (req: Request) => Response
Mixing both inconsistently in a codebase without a rule — confuses readers and makes diffs noisier.
Forgetting that interfaces can extend type aliases and vice versa — they interoperate freely.
Interoperability: Mixing Both
Interfaces and type aliases are interoperable. An interface can extend a type alias, and a type alias can intersect an interface. You are not locked into one world.
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
// Interface extending a type alias
interface Article extends Timestamped {
id: string;
title: string;
body: string;
}
// Type alias intersecting an interface
interface Named {
firstName: string;
lastName: string;
}
type Employee = Named & {
employeeId: number;
department: string;
};
// Generic interface using a type alias constraint
type Identifiable = { id: string | number };
interface Repository<T extends Identifiable> {
findById(id: T['id']): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: T['id']): Promise<void>;
}
Real-World Example: Building a Form Library
Here is a realistic scenario that uses both tools for what they are each best at:
// type alias for union — field variants
type FieldType = 'text' | 'email' | 'password' | 'number' | 'select' | 'checkbox';
// type alias for mapped type — make all fields optional for partial updates
type PartialForm<T> = {
[K in keyof T]?: T[K] extends object ? PartialForm<T[K]> : T[K];
};
// interface for the core shape — will be extended by consumers
interface FormField {
name: string;
label: string;
type: FieldType;
required?: boolean;
placeholder?: string;
}
// interface extends interface
interface SelectField extends FormField {
type: 'select';
options: Array<{ value: string; label: string }>;
multiple?: boolean;
}
// type alias for discriminated union — the full field union
type AnyField =
| FormField
| SelectField
| (FormField & { type: 'checkbox'; defaultChecked?: boolean });
// interface for the form config — public API, extendable
interface FormConfig {
id: string;
fields: AnyField[];
onSubmit: (values: Record<string, unknown>) => Promise<void>;
}
// consumers can augment FormConfig via declaration merging
// e.g. in a theme plugin:
// interface FormConfig { theme?: 'light' | 'dark' }
function buildForm(config: FormConfig): HTMLFormElement {
const form = document.createElement('form');
form.id = config.id;
// ... render fields
return form;
}
FormField, FormConfig), while type aliaseshandle unions (FieldType, AnyField) and mapped types (PartialForm). Using each for its strengths gives you the best of both worlds.Quick Reference
Reach for interface when:
- You are defining a class contract (
implements) - You expect consumers to extend or augment the type
- You are augmenting a third-party module's types
- The type is a public API boundary in a library
Reach for type when:
- You need a union (
A | B) - You need a mapped or conditional type
- You need a tuple, primitive alias, or template literal type
- You are writing utility/helper types
Either works fine when:
- Describing a plain object shape for internal use
- The type is consumed but never extended
- You just want to give a name to a function signature
The goal is not to pick one and never use the other — it is to understand the strengths of each and apply the right tool for the job.