TypeScript Type Aliases vs Interfaces: When to Use What

In TypeScript, both type aliases and interfaces are powerful tools for defining custom types. They allow developers to create reusable and self - documenting type definitions. However, they have different characteristics and use - cases. Understanding when to use type aliases and when to use interfaces is crucial for writing clean, maintainable, and type - safe TypeScript code. This blog will explore the fundamental concepts, usage methods, common practices, and best practices for type aliases and interfaces in TypeScript.

Table of Contents

  1. Fundamental Concepts
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts

Type Aliases

A type alias is a way to give a name to any type. It can represent primitive types, union types, intersection types, tuple types, and more. Type aliases are defined using the type keyword.

// Primitive type alias
type MyString = string;

// Union type alias
type StringOrNumber = string | number;

// Intersection type alias
interface Person {
    name: string;
}
interface Employee {
    employeeId: number;
}
type PersonEmployee = Person & Employee;

Interfaces

An interface is a contract that defines a shape of an object. It describes the properties and their types that an object must have. Interfaces are defined using the interface keyword.

interface Point {
    x: number;
    y: number;
}

Usage Methods

Type Aliases

  • Union and Intersection Types: Type aliases are great for creating complex types by combining other types using union (|) and intersection (&) operators.
type Admin = {
    role: 'admin';
    permissions: string[];
};
type User = {
    role: 'user';
    id: number;
};
type AdminOrUser = Admin | User;
  • Literal Types: Type aliases can be used to define literal types easily.
type Direction = 'north' | 'south' | 'east' | 'west';

Interfaces

  • Object Shapes: Interfaces are primarily used to define the shape of objects.
interface UserProfile {
    name: string;
    age: number;
    email: string;
}

const user: UserProfile = {
    name: 'John Doe',
    age: 30,
    email: '[email protected]'
};
  • Class Implementations: Interfaces can be implemented by classes, enforcing that the class adheres to a certain contract.
interface Animal {
    makeSound(): void;
}

class Dog implements Animal {
    makeSound() {
        console.log('Woof!');
    }
}

Common Practices

Type Aliases

  • When Working with Primitives and Literals: Use type aliases when you need to define custom types based on primitive types or literal values.
type PositiveNumber = number & { __positive: never };
function square(num: PositiveNumber): PositiveNumber {
    return (num * num) as PositiveNumber;
}
  • Complex Type Combinations: When creating complex types by combining multiple types, type aliases are more flexible.
type NumericArray = number[];
type StringOrNumericArray = string[] | NumericArray;

Interfaces

  • Code Sharing and Compatibility: Interfaces are great for sharing type definitions across different parts of a project, especially when multiple classes need to implement the same contract.
interface Shape {
    area(): number;
}

class Circle implements Shape {
    constructor(private radius: number) {}
    area() {
        return Math.PI * this.radius * this.radius;
    }
}

class Square implements Shape {
    constructor(private side: number) {}
    area() {
        return this.side * this.side;
    }
}

Best Practices

General Guidelines

  • Use Interfaces for Object Shapes: If you are defining the shape of an object, especially when it comes to classes implementing a contract, use interfaces.
  • Use Type Aliases for Complex Types: For complex type combinations like union and intersection types, literal types, and types based on primitives, use type aliases.
  • Be Consistent: Within a project, try to be consistent in your use of type aliases and interfaces. This makes the codebase more predictable and easier to understand.

Specific Scenarios

  • Extending and Merging: Interfaces can be extended and merged, which is useful for adding new properties to an existing interface.
interface BaseUser {
    name: string;
}
interface ExtendedUser extends BaseUser {
    age: number;
}
  • Using Type Aliases for Type Guards: Type aliases can be used in combination with type guards to narrow down types.
type AnimalType = 'dog' | 'cat';
interface Dog {
    type: 'dog';
    bark(): void;
}
interface Cat {
    type: 'cat';
    meow(): void;
}
type Animal = Dog | Cat;

function isDog(animal: Animal): animal is Dog {
    return animal.type === 'dog';
}

Conclusion

Type aliases and interfaces in TypeScript are both valuable tools for defining custom types. Type aliases are more flexible and suitable for creating complex types, working with primitives and literals, and using in type guards. Interfaces, on the other hand, are ideal for defining object shapes, sharing type definitions, and enforcing contracts in classes. By understanding their differences and following best practices, developers can write more robust and maintainable TypeScript code.

References