Interfaces and Types
TypeScript gives you two powerful tools for defining data shapes: interfaces and type aliases. Understanding when to use each is essential for clean, maintainable code.
Interfaces vs Type Aliases
Both can describe object shapes, but they differ in important ways.
// Interface
interface User {
id: number;
name: string;
email: string;
}
// Type alias
type Product = {
id: number;
name: string;
price: number;
};
const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
const product: Product = { id: 101, name: "Keyboard", price: 79.99 };
console.log(`User: ${user.name} (${user.email})`);
console.log(`Product: ${product.name} - $${product.price}`);
// Type aliases can represent primitives, unions, and tuples
type ID = string | number;
type Coordinate = [number, number];
type Status = "active" | "inactive" | "pending";
const userId: ID = "abc-123";
const point: Coordinate = [10, 20];
const status: Status = "active";
console.log(`ID: ${userId}, Point: (${point[0]}, ${point[1]}), Status: ${status}`);
Extending Interfaces
Interfaces can extend other interfaces to build complex types from simpler ones.
interface Animal {
name: string;
age: number;
}
interface Pet extends Animal {
owner: string;
vaccinated: boolean;
}
interface Dog extends Pet {
breed: string;
tricks: string[];
}
const myDog: Dog = {
name: "Max",
age: 3,
owner: "Alice",
vaccinated: true,
breed: "Golden Retriever",
tricks: ["sit", "shake", "roll over"]
};
console.log(`${myDog.name} is a ${myDog.breed}`);
console.log(`Owner: ${myDog.owner}, Age: ${myDog.age}`);
console.log(`Tricks: ${myDog.tricks.join(", ")}`);
// Extending multiple interfaces
interface Serializable {
toJSON(): string;
}
interface Printable {
display(): string;
}
interface Document extends Serializable, Printable {
title: string;
content: string;
}
const doc: Document = {
title: "Meeting Notes",
content: "Discussed quarterly goals.",
toJSON() {
return JSON.stringify({ title: this.title, content: this.content });
},
display() {
return `${this.title}: ${this.content}`;
}
};
console.log(`\nDocument: ${doc.display()}`);
console.log(`JSON: ${doc.toJSON()}`);
Intersection Types
Type aliases use & to combine types, similar to interface extension.
type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };
// Combine with intersection
type Person = HasName & HasAge & HasEmail;
const person: Person = {
name: "Bob",
age: 28,
email: "bob@example.com"
};
console.log(`${person.name}, age ${person.age}, email: ${person.email}`);
// Intersection with inline types
type Employee = Person & {
department: string;
salary: number;
};
const employee: Employee = {
name: "Carol",
age: 35,
email: "carol@company.com",
department: "Engineering",
salary: 95000
};
console.log(`\n${employee.name} works in ${employee.department}`);
console.log(`Salary: $${employee.salary}`);
// Generic response wrapper
type ApiResponse<T> = {
data: T;
status: number;
timestamp: string;
};
type UserData = { id: number; username: string };
type UserResponse = ApiResponse<UserData>;
const response: UserResponse = {
data: { id: 1, username: "alice" },
status: 200,
timestamp: new Date().toISOString()
};
console.log(`\nAPI Response: status ${response.status}`);
console.log(`User: ${response.data.username}`);
Declaration Merging
Interfaces support declaration merging — declaring the same interface name twice merges them. Type aliases cannot do this.
// Declaration merging
interface Config {
apiUrl: string;
timeout: number;
}
interface Config {
retryCount: number;
debug: boolean;
}
// Merged Config has all four properties
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryCount: 3,
debug: false
};
console.log(`API URL: ${config.apiUrl}`);
console.log(`Timeout: ${config.timeout}ms`);
console.log(`Retries: ${config.retryCount}`);
console.log(`Debug: ${config.debug}`);
Optional and Readonly Properties
interface UserProfile {
readonly id: number;
name: string;
email: string;
bio?: string;
avatar?: string;
readonly createdAt: string;
}
const profile: UserProfile = {
id: 1,
name: "Diana",
email: "diana@example.com",
bio: "TypeScript enthusiast",
createdAt: "2024-01-15"
};
profile.name = "Diana Smith";
// profile.id = 2; // Error: Cannot assign to read-only property
console.log(`ID: ${profile.id} (readonly)`);
console.log(`Name: ${profile.name}`);
console.log(`Bio: ${profile.bio}`);
console.log(`Avatar: ${profile.avatar ?? "not set"}`);
// Index signatures for dynamic keys
interface StringMap {
[key: string]: string;
}
const headers: StringMap = {
"Content-Type": "application/json",
"Authorization": "Bearer token123"
};
Object.entries(headers).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
Try It Yourself
// Build a vehicle type hierarchy
interface Vehicle {
make: string;
model: string;
year: number;
}
interface ElectricVehicle extends Vehicle {
batteryCapacity: number;
range: number;
}
interface GasVehicle extends Vehicle {
fuelType: "regular" | "premium" | "diesel";
mpg: number;
}
const tesla: ElectricVehicle = {
make: "Tesla", model: "Model 3", year: 2024,
batteryCapacity: 75, range: 358
};
const civic: GasVehicle = {
make: "Honda", model: "Civic", year: 2024,
fuelType: "regular", mpg: 36
};
console.log(`EV: ${tesla.year} ${tesla.make} ${tesla.model} - ${tesla.range} mi range`);
console.log(`Gas: ${civic.year} ${civic.make} ${civic.model} - ${civic.mpg} MPG`);
// Use intersection types
type Timestamps = { createdAt: string; updatedAt: string };
type SoftDeletable = { deletedAt?: string; isDeleted: boolean };
type BlogPost = {
id: number;
title: string;
author: string;
} & Timestamps & SoftDeletable;
const post: BlogPost = {
id: 1, title: "Understanding Types", author: "Alice",
createdAt: "2024-01-15", updatedAt: "2024-01-16", isDeleted: false
};
console.log(`\n"${post.title}" by ${post.author}`);
console.log(`Created: ${post.createdAt}, Deleted: ${post.isDeleted}`);
Key Takeaways
- Interfaces define object shapes and support declaration merging
- Type aliases can represent any type: objects, primitives, unions, tuples, intersections
- Use
extendsfor interface hierarchies and&for type intersections - Declaration merging is unique to interfaces — useful for extending third-party types
- Both support optional (
?) andreadonlymodifiers - Prefer interfaces for public APIs, type aliases for unions and complex compositions
Pro Tip: Start with interfaces for object shapes that might be extended or implemented by classes. Use type aliases for unions, intersections, and computed types. When in doubt, start with an interface — you can always switch later.
Next Steps
You've seen how interfaces and type aliases define fixed shapes. But what if you want a single function or interface to work with many different types without losing type safety? That's where generics come in.
Next lesson
Generics
Write reusable, type-safe code with TypeScript generics. Learn generic functions, interfaces, classes, and constraints.
25 min