TypeScript Interfaces
TypeScript interfaces are a powerful way to define contracts within your code and to provide explicit naming of types. They help enforce structure in your codebase and make your code more maintainable and scalable.
What Are Interfaces?
In TypeScript, an interface is a way to define the shape of an object. It acts as a contract that ensures specific properties and methods exist on an object, with the correct types and signatures. Unlike traditional object-oriented languages, TypeScript interfaces are purely for type checking during development and don't exist at runtime after compilation to JavaScript.
Let's start with a basic interface:
interface Person {
firstName: string;
lastName: string;
age: number;
}
This interface states that any object of type Person
must have three properties: firstName
and lastName
(both strings), and age
(a number).
Using Interfaces
Once you've defined an interface, you can use it to type check your objects:
// Creating an object that adheres to the Person interface
const john: Person = {
firstName: "John",
lastName: "Doe",
age: 30
};
// This will cause a compile-time error because 'age' is missing
const invalidPerson: Person = {
firstName: "Jane",
lastName: "Doe"
// Error: Property 'age' is missing in type '{ firstName: string; lastName: string; }' but required in type 'Person'.
};
// This will also cause an error because 'age' is of the wrong type
const anotherInvalidPerson: Person = {
firstName: "Bob",
lastName: "Smith",
age: "thirty" // Error: Type 'string' is not assignable to type 'number'.
};
Optional Properties
Sometimes you might want certain properties to be optional. You can use the ?
syntax to achieve this:
interface Contact {
name: string;
email: string;
phone?: string; // Optional property
address?: string; // Optional property
}
// Both of these are valid Contact objects
const contact1: Contact = {
name: "Alice",
email: "[email protected]"
};
const contact2: Contact = {
name: "Bob",
email: "[email protected]",
phone: "123-456-7890",
address: "123 Main St"
};
Readonly Properties
If you want to make sure properties can't be modified after object creation, you can use the readonly
modifier:
interface Config {
readonly apiKey: string;
readonly apiEndpoint: string;
debugMode: boolean; // This can be changed
}
const appConfig: Config = {
apiKey: "abc123",
apiEndpoint: "https://api.example.com",
debugMode: false
};
// This is allowed
appConfig.debugMode = true;
// This will cause an error
// appConfig.apiKey = "xyz789";
// Error: Cannot assign to 'apiKey' because it is a read-only property.
Function Types in Interfaces
Interfaces can also describe the shape of functions:
interface MathFunction {
(x: number, y: number): number;
}
// Function implementing the interface
const add: MathFunction = function(x: number, y: number): number {
return x + y;
};
const subtract: MathFunction = (x, y) => x - y; // Type inference works here
console.log(add(5, 3)); // Output: 8
console.log(subtract(10, 4)); // Output: 6
Method Signatures in Interfaces
You can define methods in interfaces as well:
interface Vehicle {
make: string;
model: string;
year: number;
start(): void;
stop(): void;
accelerate(speed: number): void;
}
class Car implements Vehicle {
make: string;
model: string;
year: number;
private isRunning: boolean = false;
constructor(make: string, model: string, year: number) {
this.make = make;
this.model = model;
this.year = year;
}
start(): void {
this.isRunning = true;
console.log("Car started");
}
stop(): void {
this.isRunning = false;
console.log("Car stopped");
}
accelerate(speed: number): void {
if (this.isRunning) {
console.log(`Accelerating to ${speed} mph`);
} else {
console.log("Cannot accelerate. Car is not running.");
}
}
}
const myCar = new Car("Toyota", "Corolla", 2020);
myCar.start(); // Output: Car started
myCar.accelerate(60); // Output: Accelerating to 60 mph
myCar.stop(); // Output: Car stopped
Extending Interfaces
Interfaces can extend other interfaces, allowing you to build more complex types from simpler ones:
interface BasicPerson {
name: string;
age: number;
}
interface Employee extends BasicPerson {
employeeId: string;
department: string;
salary: number;
}
// An Employee object must have all required fields from both interfaces
const employee: Employee = {
name: "Sarah Johnson",
age: 35,
employeeId: "EMP001",
department: "Engineering",
salary: 90000
};
You can even extend multiple interfaces:
interface HasAddress {
address: string;
}
interface HasContact {
email: string;
phone: string;
}
interface Customer extends BasicPerson, HasAddress, HasContact {
customerId: string;
loyaltyPoints: number;
}
// A Customer must have all properties from all interfaces
const customer: Customer = {
name: "Michael Brown",
age: 42,
address: "456 Oak Avenue",
email: "[email protected]",
phone: "555-123-4567",
customerId: "CUST789",
loyaltyPoints: 250
};
Interface vs Type Alias
TypeScript offers both interfaces and type aliases for defining custom types. While they have many similarities, there are some key differences:
Here's a brief comparison:
// Interface
interface UserInterface {
id: number;
name: string;
}
// Equivalent Type Alias
type UserType = {
id: number;
name: string;
};
The main practical differences:
- Declaration merging: Interfaces with the same name are automatically merged.
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
// Box now has all three properties
const box: Box = {
height: 5,
width: 10,
length: 20
};
- Type aliases can represent more complex types like unions, primitives, and tuples directly:
// Union type
type Status = "pending" | "approved" | "rejected";
// Primitive
type ID = string;
// Tuple
type Coordinates = [number, number];
For most object type definitions, interfaces are recommended because they're more extensible and clearer in error messages.
Real-World Example: Building a REST API Client
Here's how interfaces can be used in a practical scenario - building a strongly typed API client:
// API Response Interfaces
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
interface User {
id: number;
username: string;
email: string;
role: "admin" | "user";
createdAt: string;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
createdAt: string;
updatedAt: string;
}
// API Client Interface
interface ApiClient {
getUser(id: number): Promise<ApiResponse<User>>;
getUserPosts(userId: number): Promise<ApiResponse<Post[]>>;
createPost(post: Omit<Post, "id" | "createdAt" | "updatedAt">): Promise<ApiResponse<Post>>;
updatePost(id: number, post: Partial<Omit<Post, "id" | "createdAt" | "updatedAt">>): Promise<ApiResponse<Post>>;
deletePost(id: number): Promise<ApiResponse<{deleted: boolean}>>;
}
// Implementation of the API Client
class ApiClientImpl implements ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async getUser(id: number): Promise<ApiResponse<User>> {
// Imagine this is making a real fetch call
console.log(`GET ${this.baseUrl}/users/${id}`);
// Placeholder for demo purposes
return {
success: true,
data: {
id,
username: "johndoe",
email: "[email protected]",
role: "user",
createdAt: new Date().toISOString()
}
};
}
async getUserPosts(userId: number): Promise<ApiResponse<Post[]>> {
console.log(`GET ${this.baseUrl}/users/${userId}/posts`);
return {
success: true,
data: [
{
id: 1,
title: "First Post",
content: "This is the first post content",
authorId: userId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
};
}
async createPost(post: Omit<Post, "id" | "createdAt" | "updatedAt">): Promise<ApiResponse<Post>> {
console.log(`POST ${this.baseUrl}/posts`, post);
return {
success: true,
data: {
id: Math.floor(Math.random() * 1000),
...post,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
};
}
async updatePost(id: number, post: Partial<Omit<Post, "id" | "createdAt" | "updatedAt">>): Promise<ApiResponse<Post>> {
console.log(`PUT ${this.baseUrl}/posts/${id}`, post);
return {
success: true,
data: {
id,
title: post.title || "Updated Post",
content: post.content || "Updated content",
authorId: post.authorId || 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
};
}
async deletePost(id: number): Promise<ApiResponse<{deleted: boolean}>> {
console.log(`DELETE ${this.baseUrl}/posts/${id}`);
return {
success: true,
data: { deleted: true }
};
}
}
// Using the API client
async function main() {
const api = new ApiClientImpl("https://api.example.com");
// Type safety ensures we use the correct parameters and get typed responses
const userResponse = await api.getUser(1);
if (userResponse.success) {
console.log(`Hello, ${userResponse.data.username}!`);
const postsResponse = await api.getUserPosts(userResponse.data.id);
if (postsResponse.success) {
console.log(`Found ${postsResponse.data.length} posts`);
}
const newPost = await api.createPost({
title: "Using TypeScript Interfaces",
content: "Interfaces make our API calls type-safe!",
authorId: userResponse.data.id
});
if (newPost.success) {
console.log(`Created post with ID: ${newPost.data.id}`);
}
}
}
main().catch(error => console.error("Error:", error));
Summary
Interfaces are one of TypeScript's most powerful features, providing a way to define contracts within your code. They help catch errors during development instead of at runtime, making your code more robust.
Key things to remember about interfaces:
- They define the shape of objects without implementation details
- They enforce object structure at compile time
- They can represent function types, object shapes, and class contracts
- They can be extended to build more complex types from simpler ones
- They support optional properties, readonly properties, and method signatures
- They don't exist at runtime in your JavaScript code
By using interfaces effectively, you can make your TypeScript code more maintainable, self-documenting, and less prone to bugs.
Exercises
To practice working with interfaces:
- Create an interface for a
Product
with properties for name, price, description, and inventory count. - Create a
ShoppingCart
interface with methods for adding products, removing products, and calculating the total price. - Implement a class that uses your
ShoppingCart
interface. - Create a hierarchical set of interfaces: start with
Animal
, extend it to createMammal
andBird
, and further extend those. - Design interfaces for a simple blog application with users, posts, and comments.
Additional Resources
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)