TypeScript Interfaces vs Type Aliases
debt(d3/e1/b3/t5)
Closest to 'default linter catches the common case' (d3). The detection_hints list TypeScript compiler and ESLint as tools. ESLint with @typescript-eslint rules (e.g. consistent-type-definitions) can flag inconsistent use of interface vs type for object shapes, catching the most common mistakes automatically at lint time.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix states 'both work for most cases' and the fix is simply swapping `type` for `interface` (or vice versa) at the declaration site — a single-keyword change per occurrence with no cross-cutting refactor needed.
Closest to 'localised tax' (b3). The choice applies to web and cli contexts and is tagged as syntax/types. Mixing conventions (the noted common mistake) creates a persistent but localised cognitive tax — it mainly affects the files where object shapes are declared and any consumer that needs declaration merging, not the entire architecture.
Closest to 'notable trap' (t5). The misconception field explicitly states developers believe interfaces and type aliases are completely interchangeable — a documented gotcha. The real differences (declaration merging vs union support) are non-obvious but well-documented, and most developers encounter this eventually when trying to augment third-party types or express union shapes.
Also Known As
TL;DR
Explanation
Interfaces (interface User {}) can be extended with extends and are automatically merged if declared twice in the same scope (useful for augmenting library types). Type aliases (type User = {}) support union types, intersection types, conditional types, and mapped types — things interfaces cannot express. Convention: prefer interface for object shapes that may be extended or implemented by classes; prefer type for unions, intersections, and computed types. In practice, both work for most cases.
Diagram
flowchart TD
subgraph Interface
INT[interface User<br/>name: string<br/>email: string]
INT_MERGE[Can be merged<br/>declaration merging]
INT_EXTEND[extends keyword<br/>for inheritance]
end
subgraph Type_Alias
TYPE[type User =<br/>name: string<br/>email: string]
TYPE_UNION[Can use union types<br/>string or number]
TYPE_MAPPED[Can use mapped types<br/>Partial Required etc]
end
subgraph When_to_Use
USE_INT[Interface: public API<br/>class contracts<br/>extendable shapes]
USE_TYPE[Type: unions intersections<br/>computed types<br/>primitives]
end
style INT fill:#1f6feb,color:#fff
style TYPE fill:#238636,color:#fff
style USE_INT fill:#1f6feb,color:#fff
style USE_TYPE fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Using type for object shapes that need to be extended or implemented by classes — interface is clearer.
- Using interface for union types — interfaces cannot express string | number directly; type is required.
- Not using declaration merging for extending third-party library types — add fields to window or Express Request with interface merging.
- Mixing conventions within a codebase — pick interface for objects and type for everything else, consistently.
Code Examples
// type cannot be merged for library augmentation:
type Request = { user?: User }; // Type — cannot merge
// Cannot augment Express's Request type this way
// interface cannot express union:
interface StringOrNumber = string | number; // Syntax error
// Interface for object shapes (extendable, mergeable):
interface User { id: number; name: string; }
interface Admin extends User { permissions: string[]; } // Extension
// Augment Express Request (declaration merging):
declare global {
namespace Express { interface Request { user?: User; } }
}
// Type for unions, intersections, computed:
type ID = string | number;
type ApiResponse<T> = { data: T; status: number; };
type UserOrAdmin = User | Admin;