TypeScript Interfaces vs. Types: Choosing the Right Tool for Your Code

November 2, 2024 (2w ago)

TypeScript Interfaces vs. Types: Choosing the Right Tool for Your Code

When working with TypeScript, defining data structures, function signatures, and other contracts is key to creating robust, type-safe applications. Interfaces and types are two ways to define these structures, but each has unique strengths and use cases. This guide will help you understand the differences between interfaces and types, when to use each, and how they can improve code readability and maintainability.


What is a Type Alias in TypeScript?

A type alias allows you to define a custom name for a type, which can represent any kind of data structure, including primitives, objects, and even unions of multiple types.

Example of a Type Alias

type ID = number | string;
type User = {
  id: ID;
  name: string;
  age: number;
};
 
const user: User = { id: 1, name: "Alice", age: 25 };

In this example, ID is a type alias that allows id to be either a number or string. Type aliases like ID can be especially useful for unions and complex data structures.


What is an Interface in TypeScript?

An interface in TypeScript is a way to define the shape of an object, specifying the properties and their types. Interfaces can describe object shapes, and they can also be extended to create complex types.

Example of an Interface

interface User {
  id: number;
  name: string;
  age: number;
}
 
const user: User = { id: 1, name: "Bob", age: 30 };

Here, User defines a contract that user objects must follow. If a User object doesn’t match this structure, TypeScript will raise an error.


Key Differences Between Interfaces and Types

While both interfaces and types can define data structures, there are some important differences:

Feature Interface Type Alias
Extensibility Can be extended by other interfaces Cannot be extended directly but can be combined with intersections
Declaration Merging Supports declaration merging Does not support declaration merging
Usable for Primitives No Yes (for primitive types like string, number)
Use for Function Types Limited but possible Common and flexible
Ideal For Object shapes, APIs Complex types, unions, and intersections

Extending Interfaces vs. Types

One of the biggest differences is that interfaces can be extended to create complex structures, while types can only be combined using intersections.

Extending an Interface

interface Person {
  name: string;
  age: number;
}
 
interface Employee extends Person {
  position: string;
}
 
const employee: Employee = { name: "Alice", age: 30, position: "Developer" };

With interfaces, Employee inherits from Person, adding an additional position property.

Extending a Type with Intersections

While types don’t directly support extension, you can use intersections to combine multiple types.

type Person = {
  name: string;
  age: number;
};
 
type Employee = Person & {
  position: string;
};
 
const employee: Employee = { name: "Bob", age: 40, position: "Manager" };

Here, Employee is created by combining Person with additional properties using an intersection type.


Declaration Merging with Interfaces

One of the unique features of interfaces is declaration merging. If the same interface is declared multiple times, TypeScript will merge them into a single definition.

Example of Declaration Merging

interface Product {
  name: string;
}
 
interface Product {
  price: number;
}
 
const product: Product = { name: "Laptop", price: 1000 };

The Product interface merges the name and price properties, making it useful for extending third-party types without modifying the original.

Type aliases do not support declaration merging, making interfaces more flexible for scenarios where you need to add additional properties to existing types.


Using Types for Complex and Union Types

Types are ideal for creating unions, intersections, and other complex data structures. They allow you to define types that combine multiple types into one.

Example: Union Types with Type Aliases

type Status = "active" | "inactive" | "pending";
 
function displayStatus(status: Status): void {
  console.log("Status:", status);
}
 
displayStatus("active"); // Valid
// displayStatus("unknown"); // Error: Argument of type '"unknown"' is not assignable to parameter of type 'Status'

In this example, Status is a union of three possible string values. This provides strong type-checking without needing an enumeration or multiple checks.


Function Types with Type Aliases

Type aliases are often preferred for function types, providing a clear syntax for specifying input and output types.

Example: Function Type with Type Alias

type Greet = (name: string) => string;
 
const sayHello: Greet = (name) => `Hello, ${name}!`;
console.log(sayHello("Alice")); // Output: "Hello, Alice!"

Using a type alias for a function type can make your code more readable and modular, especially in complex applications.

Function Types with Interfaces

Although less common, you can also define function types with interfaces:

interface Greet {
  (name: string): string;
}
 
const sayHello: Greet = (name) => `Hello, ${name}!`;
console.log(sayHello("Bob")); // Output: "Hello, Bob!"

Both approaches work, but using type aliases is generally more concise and readable for function types.


Choosing Between Interface and Type Alias

When deciding between interfaces and types, consider the following guidelines:

  1. Use Interfaces for Objects and APIs: Interfaces are ideal for defining object shapes, especially when working with APIs or class-based code.
  2. Use Types for Unions and Intersections: Types are better suited for unions, intersections, and function signatures.
  3. Use Interfaces for Extensibility: Interfaces offer declaration merging and extensions, making them ideal for flexible, reusable structures.
  4. Choose Type Aliases for Primitives and Complex Types: Types are more versatile for defining complex types, unions, and primitives.

Summary Table: When to Use Interface vs. Type Alias

Use Case Recommended Tool
Object shapes and API definitions Interface
Extending existing types Interface
Declaration merging Interface
Union and intersection types Type Alias
Complex data types with unions or intersections Type Alias
Function types Type Alias (usually)
Primitive aliases (e.g., type ID = string) Type Alias

Practical Examples: Interfaces and Types in Action

Let’s explore some practical examples of interfaces and types to see how they can be applied effectively.

Example 1: API Response

For an API response, interfaces can define a predictable structure for returned data.

interface ApiResponse<T> {
  data: T;
  success: boolean;
  error?: string;
}
 
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  return fetch(url)
    .then((res) => res.json())
    .then((data) => ({ data, success: true }))
    .catch(() => ({ data: null, success: false, error: "Failed to fetch data" }));
}

Using an interface here provides a predictable structure for the API response, allowing consistent error handling and type-checking.

Example 2: Union Types for Configurations

Type aliases are useful for creating flexible configurations by combining different options.

type LogLevel = "debug" | "info" | "warn" | "error";
type LogMethod = (message: string) => void;
 
interface Logger {
  level: LogLevel;
  log: LogMethod;
}
 
const logger: Logger = {
  level: "info",
  log: (message) => console.log(`[INFO]: ${message}`)
};
 
logger.log("Application started");

Here, LogLevel is defined as a union type, making it easy to constrain the logging level to specific options.


Conclusion

TypeScript’s interfaces and type aliases are both essential tools for defining types, but each shines in different scenarios. Interfaces excel in defining object shapes and are ideal for creating extensible, reusable structures. Types, on the other hand, are flexible and powerful for unions, intersections, and complex data combinations.

By understanding the strengths and limitations of interfaces and types, you can create clean, readable, and type-safe code, making your TypeScript applications more robust and maintainable.