Back to Blog
TypeScriptInterfacesTypesBest Practices

TypeScript Interfaces vs Types

Understand when to use interfaces vs type aliases in TypeScript.

B
Bootspring Team
Engineering
September 21, 2018
7 min read

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; // OK

Primitive 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 possible

Error 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 UserType

When 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.

Share this article

Help spread the word about Bootspring