TypeScript Interfaces
Interfaces in TypeScript provide a powerful way to define the structure of objects, creating contracts that ensure consistency across your application. They serve as blueprints that describe what properties and methods an object should have, enabling better code organization, documentation, and type safety.
Unlike classes, interfaces exist only at compile time and are used purely for type checking. They help define clear API boundaries, make code more readable, and enable powerful IDE features like autocompletion and refactoring support.
Basic Interface Definition
Interfaces define the shape of objects by specifying property names and their types.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Using the interface
function createUser(userData: User): User {
return {
id: userData.id,
name: userData.name,
email: userData.email,
isActive: userData.isActive
};
}
const newUser: User = {
id: 1,
name: "Alice Johnson",
email: "[email protected]",
isActive: true
};
Optional Properties
Properties can be marked as optional using the ?
operator, making them not required when implementing the interface.
interface Product {
id: number;
name: string;
description?: string; // Optional property
price: number;
category?: string; // Optional property
inStock: boolean;
}
// Valid implementations
const product1: Product = {
id: 1,
name: "Laptop",
price: 999.99,
inStock: true
};
const product2: Product = {
id: 2,
name: "Mouse",
description: "Wireless gaming mouse",
price: 49.99,
category: "Electronics",
inStock: false
};
Readonly Properties
Properties can be marked as readonly to prevent modification after object creation.
interface Point {
readonly x: number;
readonly y: number;
}
interface Configuration {
readonly apiUrl: string;
readonly timeout: number;
retryCount?: number; // Can be modified
}
const origin: Point = { x: 0, y: 0 };
// origin.x = 1; // Error: Cannot assign to 'x' because it is a read-only property
const config: Configuration = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryCount: 3
};
// config.apiUrl = "new-url"; // Error: readonly property
config.retryCount = 5; // OK: not readonly
Function Types in Interfaces
Interfaces can define function signatures as properties or methods.
interface Calculator {
// Method syntax
add(a: number, b: number): number;
subtract(a: number, b: number): number;
// Property syntax
multiply: (a: number, b: number) => number;
divide: (a: number, b: number) => number;
}
class BasicCalculator implements Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
multiply = (a: number, b: number): number => {
return a * b;
}
divide = (a: number, b: number): number => {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
}
Index Signatures
Index signatures allow interfaces to describe objects with dynamic property names.
interface StringDictionary {
[key: string]: string;
}
interface NumberDictionary {
[key: string]: number;
}
interface FlexibleUser {
name: string;
email: string;
[key: string]: any; // Additional properties of any type
}
const translations: StringDictionary = {
hello: "Hola",
goodbye: "Adiós",
welcome: "Bienvenido"
};
const scores: NumberDictionary = {
math: 95,
science: 88,
english: 92
};
const user: FlexibleUser = {
name: "John",
email: "[email protected]",
age: 30, // Additional property
city: "New York" // Additional property
};
Interface Inheritance
Interfaces can extend other interfaces, creating hierarchical type relationships.
interface Animal {
name: string;
age: number;
}
interface Mammal extends Animal {
furColor: string;
warmBlooded: boolean;
}
interface Dog extends Mammal {
breed: string;
bark(): void;
}
const myDog: Dog = {
name: "Buddy",
age: 3,
furColor: "golden",
warmBlooded: true,
breed: "Golden Retriever",
bark() {
console.log("Woof!");
}
};
// Multiple inheritance
interface Flyable {
maxAltitude: number;
fly(): void;
}
interface Swimmable {
maxDepth: number;
swim(): void;
}
interface Duck extends Animal, Flyable, Swimmable {
quack(): void;
}
Generic Interfaces
Interfaces can be generic, allowing them to work with different types while maintaining type safety.
interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
timestamp: Date;
}
interface Repository<T, K> {
findById(id: K): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: K): Promise<boolean>;
findAll(): Promise<T[]>;
}
// Usage examples
const stringContainer: Container<string> = {
value: "Hello",
getValue() {
return this.value;
},
setValue(value: string) {
this.value = value;
}
};
const userResponse: ApiResponse<User[]> = {
success: true,
data: [newUser],
message: "Users retrieved successfully",
timestamp: new Date()
};
class UserRepository implements Repository<User, number> {
async findById(id: number): Promise<User | null> {
// Implementation
return null;
}
async save(user: User): Promise<User> {
// Implementation
return user;
}
async delete(id: number): Promise<boolean> {
// Implementation
return true;
}
async findAll(): Promise<User[]> {
// Implementation
return [];
}
}
Interfaces vs Type Aliases
While interfaces and type aliases can often be used interchangeably, they have different capabilities and use cases.
// Interface approach
interface UserInterface {
id: number;
name: string;
email: string;
}
// Type alias approach
type UserType = {
id: number;
name: string;
email: string;
};
// Interfaces can be extended and merged
interface UserInterface {
createdAt: Date; // Declaration merging
}
interface ExtendedUser extends UserInterface {
isAdmin: boolean;
}
// Type aliases are more flexible with unions
type Status = "pending" | "approved" | "rejected";
type ID = string | number;
type UserWithStatus = UserType & {
status: Status;
id: ID;
};
Implementing Interfaces in Classes
Classes can implement interfaces to ensure they conform to specific contracts.
interface Drivable {
speed: number;
start(): void;
stop(): void;
accelerate(amount: number): void;
}
interface Flyable {
altitude: number;
takeOff(): void;
land(): void;
}
class Car implements Drivable {
speed: number = 0;
start(): void {
console.log("Car engine started");
}
stop(): void {
this.speed = 0;
console.log("Car stopped");
}
accelerate(amount: number): void {
this.speed += amount;
console.log(`Car accelerated to ${this.speed} mph`);
}
}
class FlyingCar implements Drivable, Flyable {
speed: number = 0;
altitude: number = 0;
// Drivable methods
start(): void {
console.log("Flying car started");
}
stop(): void {
this.speed = 0;
console.log("Flying car stopped");
}
accelerate(amount: number): void {
this.speed += amount;
}
// Flyable methods
takeOff(): void {
this.altitude = 1000;
console.log("Flying car took off");
}
land(): void {
this.altitude = 0;
console.log("Flying car landed");
}
}
Interface Merging
TypeScript allows interfaces with the same name to be automatically merged, combining their properties.
interface Window {
title: string;
}
interface Window {
isOpen: boolean;
}
interface Window {
close(): void;
}
// All declarations are merged into one interface
const window: Window = {
title: "My Window",
isOpen: true,
close() {
this.isOpen = false;
}
};
// This is particularly useful for extending third-party library types
interface Array<T> {
last(): T | undefined;
}
Array.prototype.last = function<T>(this: T[]): T | undefined {
return this[this.length - 1];
};
Advanced Interface Patterns
Conditional Properties based on other properties:
interface BaseEvent {
type: string;
timestamp: Date;
}
interface ClickEvent extends BaseEvent {
type: "click";
x: number;
y: number;
}
interface KeyEvent extends BaseEvent {
type: "keypress";
key: string;
ctrlKey: boolean;
}
type UIEvent = ClickEvent | KeyEvent;
function handleEvent(event: UIEvent) {
switch (event.type) {
case "click":
console.log(`Clicked at ${event.x}, ${event.y}`);
break;
case "keypress":
console.log(`Key pressed: ${event.key}`);
break;
}
}
Mapped Types in Interfaces:
interface Person {
name: string;
age: number;
email: string;
}
// Create a partial version
type PartialPerson = Partial<Person>;
// Create a readonly version
type ReadonlyPerson = Readonly<Person>;
// Pick specific properties
type PersonContact = Pick<Person, "name" | "email">;
// Omit specific properties
type PersonWithoutEmail = Omit<Person, "email">;
Interfaces are fundamental to TypeScript development, providing a clean way to define contracts, ensure type safety, and create maintainable code. They enable clear communication between different parts of your application and make your code more self-documenting and easier to refactor.