Both interfaces and type aliases define object shapes, but they have key differences. Here's when to use each.
Basic Syntax#
1// Interface
2interface User {
3 id: number;
4 name: string;
5 email: string;
6}
7
8// Type alias
9type UserType = {
10 id: number;
11 name: string;
12 email: string;
13};
14
15// Both work the same for object types
16const user1: User = { id: 1, name: 'John', email: 'john@example.com' };
17const user2: UserType = { id: 2, name: 'Jane', email: 'jane@example.com' };Extension#
1// Interface extends interface
2interface Person {
3 name: string;
4 age: number;
5}
6
7interface Employee extends Person {
8 employeeId: string;
9 department: string;
10}
11
12// Interface extends multiple interfaces
13interface Manager extends Employee {
14 reports: Employee[];
15}
16
17// Type uses intersection
18type PersonType = {
19 name: string;
20 age: number;
21};
22
23type EmployeeType = PersonType & {
24 employeeId: string;
25 department: string;
26};
27
28// Type can extend interface and vice versa
29interface Admin extends PersonType {
30 permissions: string[];
31}
32
33type SuperUser = Employee & {
34 superPowers: string[];
35};Declaration Merging#
1// Interfaces merge automatically
2interface Config {
3 apiUrl: string;
4}
5
6interface Config {
7 timeout: number;
8}
9
10interface Config {
11 retries: number;
12}
13
14// Result: Config has all three properties
15const config: Config = {
16 apiUrl: 'https://api.example.com',
17 timeout: 5000,
18 retries: 3
19};
20
21// Types cannot merge - error on duplicate
22type ConfigType = {
23 apiUrl: string;
24};
25
26// Error: Duplicate identifier 'ConfigType'
27// type ConfigType = {
28// timeout: number;
29// };Augmenting Libraries#
1// Extend third-party types with declaration merging
2declare module 'express' {
3 interface Request {
4 user?: {
5 id: string;
6 role: string;
7 };
8 }
9}
10
11// Now Request has user property
12app.get('/', (req, res) => {
13 console.log(req.user?.id); // TypeScript knows about user
14});
15
16// Extend global types
17declare global {
18 interface Window {
19 myApp: {
20 version: string;
21 init(): void;
22 };
23 }
24}
25
26window.myApp.version; // OKPrimitive Types#
1// Types can alias primitives
2type ID = string | number;
3type Name = string;
4type Nullable<T> = T | null;
5
6// Interfaces cannot alias primitives
7// interface ID = string; // Error
8
9// Types for literal types
10type Direction = 'north' | 'south' | 'east' | 'west';
11type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
12
13// Types for tuples
14type Point = [number, number];
15type RGB = [number, number, number];
16
17// Interfaces can't do this directly
18// interface Point = [number, number]; // Error
19
20// But can extend tuple
21interface NamedPoint extends Array<number> {
22 0: number;
23 1: number;
24 name: string;
25}Union and Intersection#
1// Types excel at unions
2type Result = Success | Error;
3type Status = 'pending' | 'active' | 'completed';
4type StringOrNumber = string | number;
5
6// Interfaces can't create unions
7// interface Result = Success | Error; // Error
8
9// But both work with intersection
10type CombinedType = TypeA & TypeB;
11
12interface Combined extends InterfaceA, InterfaceB {}
13
14// Conditional types (types only)
15type NonNullable<T> = T extends null | undefined ? never : T;
16type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
17
18// Mapped types (types only)
19type Readonly<T> = {
20 readonly [P in keyof T]: T[P];
21};
22
23type Partial<T> = {
24 [P in keyof T]?: T[P];
25};Functions#
1// Both work for function types
2interface GreetFunction {
3 (name: string): string;
4}
5
6type GreetFunctionType = (name: string) => string;
7
8// Usage is identical
9const greet1: GreetFunction = (name) => `Hello, ${name}`;
10const greet2: GreetFunctionType = (name) => `Hello, ${name}`;
11
12// Interface with call signature and properties
13interface Callable {
14 (x: number): number;
15 description: string;
16}
17
18const double: Callable = (x) => x * 2;
19double.description = 'Doubles a number';
20
21// Type equivalent
22type CallableType = {
23 (x: number): number;
24 description: string;
25};Classes#
1// Interface for class implementation
2interface Serializable {
3 serialize(): string;
4 deserialize(data: string): void;
5}
6
7class User implements Serializable {
8 serialize() {
9 return JSON.stringify(this);
10 }
11
12 deserialize(data: string) {
13 Object.assign(this, JSON.parse(data));
14 }
15}
16
17// Type works too
18type SerializableType = {
19 serialize(): string;
20 deserialize(data: string): void;
21};
22
23class Product implements SerializableType {
24 serialize() { return ''; }
25 deserialize(data: string) {}
26}
27
28// Interface can extend class
29class Animal {
30 name: string = '';
31}
32
33interface Dog extends Animal {
34 breed: string;
35}Generics#
1// Both support generics
2interface Box<T> {
3 value: T;
4}
5
6type BoxType<T> = {
7 value: T;
8};
9
10// Generic constraints
11interface Container<T extends object> {
12 data: T;
13}
14
15type ContainerType<T extends object> = {
16 data: T;
17};
18
19// Default type parameters
20interface Response<T = unknown> {
21 data: T;
22 status: number;
23}
24
25type ResponseType<T = unknown> = {
26 data: T;
27 status: number;
28};Recursive Types#
1// Both support recursive definitions
2interface TreeNode {
3 value: number;
4 children: TreeNode[];
5}
6
7type TreeNodeType = {
8 value: number;
9 children: TreeNodeType[];
10};
11
12// JSON type (recursive)
13type Json =
14 | string
15 | number
16 | boolean
17 | null
18 | Json[]
19 | { [key: string]: Json };
20
21// Linked list
22interface ListNode<T> {
23 value: T;
24 next: ListNode<T> | null;
25}Performance#
1// Interfaces are generally faster to type-check
2// Because they're cached by name
3
4// Complex type intersections can be slow
5type Complex = A & B & C & D & E & F & G;
6
7// Interface extension is optimized
8interface Efficient extends A, B, C, D, E, F, G {}
9
10// For very large codebases, prefer interfaces
11// for object types when possibleError Messages#
1// Interface errors show interface name
2interface User {
3 name: string;
4}
5
6const user: User = { names: '' };
7// Error: Property 'name' is missing in type...
8// Shows: User
9
10// Type errors might show expanded type
11type UserType = { name: string };
12
13const user2: UserType = { names: '' };
14// Error might show: { name: string } instead of UserTypeWhen to Use Each#
1// USE INTERFACE:
2// - Object shapes (main use case)
3// - When you need declaration merging
4// - Public API definitions
5// - Class implementations
6// - When extending/implementing
7
8interface ApiResponse {
9 data: unknown;
10 status: number;
11}
12
13interface Service {
14 fetch(): Promise<ApiResponse>;
15}
16
17// USE TYPE:
18// - Unions and intersections
19// - Primitives and tuples
20// - Mapped types
21// - Conditional types
22// - Complex type operations
23
24type Result<T> = Success<T> | Failure;
25type Nullable<T> = T | null;
26type Keys = keyof SomeInterface;
27type Readonly<T> = { readonly [K in keyof T]: T[K] };Best Practices#
Use Interface When:
✓ Defining object shapes
✓ Creating public APIs
✓ Need declaration merging
✓ Implementing in classes
Use Type When:
✓ Creating union types
✓ Working with primitives
✓ Using mapped/conditional types
✓ Creating type utilities
General Guidelines:
✓ Be consistent in your codebase
✓ Interface for objects, type for everything else
✓ Document complex types
✓ Prefer interface for public APIs
Avoid:
✗ Mixing without reason
✗ Over-engineering types
✗ Ignoring readability
✗ Complex nested intersections
Conclusion#
Both interfaces and types are powerful tools. Use interfaces for object shapes, class contracts, and public APIs. Use types for unions, primitives, tuples, and advanced type operations. For simple object types, either works - just be consistent. When in doubt, start with interface and switch to type if you need its unique features.