Type Aliases vs Interfaces
Introduction
TypeScript offers two main ways to name types: interface for object-like contracts and type for aliases of any type shape. They overlap for plain objects, which confuses beginners. This chapter clarifies syntax differences, capabilities, and practical rules for choosing one style consistently.
Prerequisites
Defining the Same Shape Two Ways
Object shape with interface:
interface User {
id: number;
name: string;
}Same shape with type:
type User = {
id: number;
name: string;
};For many teams, these are interchangeable for simple objects. Differences appear with unions, extension, and advanced composition.
What Only type Can Express (Easily)
Unions
type Result = "success" | "error";
type Id = string | number;Tuples
type Pair = [number, number];Primitives and intersections combined
type NonEmptyString = string & { readonly __brand: unique symbol };(Branded types are an advanced pattern; the point is interface cannot alias a primitive alone.)
Mapped and conditional types (preview)
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};Utility-style transformations almost always use type.
What interface Emphasizes
Declaration merging
interface Env {
NODE_ENV: string;
}
interface Env {
PORT: string;
}
// Env has NODE_ENV and PORTtype aliases cannot merge redeclarations—duplicate type Env names error.
extends for object inheritance
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}With type, use intersection:
type Dog = Animal & {
breed: string;
};Both work; interface + extends often reads clearer for object hierarchies.
Intersection with type
type Timestamped = {
createdAt: string;
};
type Post = Timestamped & {
id: number;
title: string;
};Code explanation:
&combines members; conflicts on the same property must be compatible- Large intersections can become hard to read—split with
interfacewhen it helps
implements in Classes
Both work with classes:
Side-by-Side Comparison
| Feature | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Extend / merge objects | extends, declaration merging | &, no merging |
| Union / literal unions | No (not directly) | Yes |
| Tuple | No | Yes |
| Primitive alias | No | Yes |
| Mapped / conditional types | No | Yes |
| Error messages on objects | Often slightly clearer | Good |
Style Guide: Practical Rules
- Object models in app/domain code — prefer
interface(User,Order,ApiClient). - Unions, tuples, function unions — use
type(Result,Route,Handler). - Composition of unions with objects — use
type:
type LoadingState = { status: "loading" };
type SuccessState<T> = { status: "success"; data: T };
type ErrorState = { status: "error"; message: string };
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;- Public library API — pick one style per exported concept and document it; consistency beats dogma.
- Do not duplicate the same shape as both
interface Userandtype Userin one codebase.
Tip
Team Consistency Wins
TypeScript does not require one winner. Align with your linter (ESLint @typescript-eslint/consistent-type-definitions) or team guide.
Converting Between Styles
Interface to type (manual):
interface Point {
x: number;
y: number;
}
type PointAlias = Point;Type object to interface (only for plain objects):
type PointType = { x: number; y: number };
// Cannot auto-convert; redefine if you need interface merging
interface PointInterface {
x: number;
y: number;
}Example: Choosing in a Small Module
Code explanation:
PaymentMethodandPaymentResultneed unions →typeCustomerandPaymentServiceare object contracts →interface- Narrowing
result.okis a discriminated union pattern (detailed in a later chapter)
Anti-Patterns
| Anti-pattern | Why it hurts |
|---|---|
type User = { ... } and interface User { ... } together | Name collision, confusion |
| `interface A | B` |
Giant type intersections instead of interface extends | Harder errors, slower editor |
Using any in aliases | Defeats purpose of naming types |
FAQ
Which does React use for props?
Both appear: interface Props is common; type Props = { ... } is equally valid. Function components with unions often prefer type.
Can I extend a type alias interface-style?
Use intersection: type B = A & { extra: true }. For interface, use extends.
Performance difference?
Compile-time only; no runtime difference. Very large mapped types may affect checker speed—rare in app code.
Does export type differ from export interface?
Both export names. With isolatedModules, export type may be required for type-only re-exports in some bundler setups (export type { User }).
eslint says “Use type only” — must I obey?
Follow team standards. The guidance is consistency, not a language limitation.
When should I use Record<string, T>?
Record is a built-in generic alias for string-keyed objects—great for dictionaries; see utility types chapter for details.