TypeScript

Type Aliases & Interfaces

25 min Lesson 6 of 40

Type Aliases & Interfaces

TypeScript provides two primary ways to define custom types: type aliases and interfaces. Both allow you to create reusable type definitions, but they have subtle differences in capabilities and use cases. Understanding when to use each is crucial for writing clean, maintainable TypeScript code.

Type Aliases

A type alias creates a new name for any type. You define it using the type keyword followed by the alias name and the type definition.

Basic Type Alias:

// Simple type alias for primitive
type UserID = string | number;

// Type alias for object shape
type User = {
  id: UserID;
  name: string;
  email: string;
  age?: number;
};

// Using the type alias
const user1: User = {
  id: 123,
  name: 'John Doe',
  email: 'john@example.com'
};

const user2: User = {
  id: 'abc-456',
  name: 'Jane Smith',
  email: 'jane@example.com',
  age: 28
};
Note: Type aliases can represent any type, including primitives, unions, tuples, and complex object shapes. They're particularly useful for union types and complex type compositions.

Type Aliases for Complex Types

Type aliases excel at representing complex type combinations:

Complex Type Aliases:

// Union type alias
type Status = 'pending' | 'approved' | 'rejected';

// Tuple type alias
type Coordinate = [number, number];

// Function type alias
type MathOperation = (a: number, b: number) => number;

// Complex object with nested types
type Product = {
  id: string;
  name: string;
  price: number;
  category: 'electronics' | 'clothing' | 'food';
  tags: string[];
  metadata: {
    createdAt: Date;
    updatedAt: Date;
    views: number;
  };
};

// Using function type alias
const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;

// Using complex type
const laptop: Product = {
  id: 'prod-001',
  name: 'MacBook Pro',
  price: 2499,
  category: 'electronics',
  tags: ['computer', 'apple', 'laptop'],
  metadata: {
    createdAt: new Date(),
    updatedAt: new Date(),
    views: 1250
  }
};

Interfaces

An interface is a way to define the shape of an object type. Interfaces use the interface keyword and are specifically designed for object shapes.

Basic Interface:

// Interface definition
interface Person {
  firstName: string;
  lastName: string;
  age: number;
  email?: string; // Optional property
  readonly id: string; // Cannot be modified after creation
}

// Using the interface
const person: Person = {
  id: 'person-123',
  firstName: 'Alice',
  lastName: 'Johnson',
  age: 30
};

// person.id = 'new-id'; // Error: Cannot assign to 'id' because it is a read-only property

// Method in interface
interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

const calc: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};
Tip: Use readonly for properties that should only be set during initialization. This helps prevent accidental modifications and makes your code more predictable.

Extending Interfaces

One of the most powerful features of interfaces is the ability to extend other interfaces using the extends keyword. This allows you to build complex type hierarchies.

Interface Extension:

// Base interface
interface Animal {
  name: string;
  age: number;
  makeSound(): void;
}

// Extended interface
interface Dog extends Animal {
  breed: string;
  wagTail(): void;
}

// Interface extending multiple interfaces
interface Bird {
  wingspan: number;
  canFly: boolean;
}

interface FlyingAnimal extends Animal, Bird {
  altitude: number;
}

// Implementation
const myDog: Dog = {
  name: 'Buddy',
  age: 5,
  breed: 'Golden Retriever',
  makeSound: () => console.log('Woof!'),
  wagTail: () => console.log('*tail wagging*')
};

const eagle: FlyingAnimal = {
  name: 'Eagle',
  age: 3,
  wingspan: 2.5,
  canFly: true,
  altitude: 1000,
  makeSound: () => console.log('Screech!')
};

Intersection Types

Intersection types combine multiple types into one using the & operator. The resulting type has all properties from all combined types.

Intersection Types with Type Aliases:

// Individual types
type Identifiable = {
  id: string;
};

type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type Versioned = {
  version: number;
};

// Intersection type combining all three
type Entity = Identifiable & Timestamped & Versioned;

// Usage
const article: Entity = {
  id: 'article-001',
  createdAt: new Date('2024-01-01'),
  updatedAt: new Date('2024-02-14'),
  version: 3
};

// Intersection with object literals
type Name = {
  firstName: string;
  lastName: string;
};

type Contact = {
  email: string;
  phone: string;
};

type Employee = Name & Contact & {
  employeeId: string;
  department: string;
};

const employee: Employee = {
  firstName: 'Sarah',
  lastName: 'Williams',
  email: 'sarah@company.com',
  phone: '+1234567890',
  employeeId: 'EMP-789',
  department: 'Engineering'
};
Note: Intersection types create a type that must satisfy ALL combined types. If there are conflicting properties, the intersection may result in never type.

Type Aliases vs Interfaces: Key Differences

While both can define object shapes, there are important differences between type aliases and interfaces:

1. Declaration Merging (Interfaces Only):

// Interfaces can be merged
interface Window {
  title: string;
}

interface Window {
  size: number;
}

// Both declarations merge into one
const window: Window = {
  title: 'My Window',
  size: 1024
};

// Type aliases CANNOT be merged
type Config = {
  theme: string;
};

// Error: Duplicate identifier 'Config'
// type Config = {
//   language: string;
// };
2. Extending vs Intersection:

// Interface extension (using extends)
interface BaseUser {
  id: string;
  name: string;
}

interface AdminUser extends BaseUser {
  permissions: string[];
}

// Type alias intersection (using &)
type BaseUserType = {
  id: string;
  name: string;
};

type AdminUserType = BaseUserType & {
  permissions: string[];
};

// Both achieve similar results
const admin1: AdminUser = {
  id: 'admin-1',
  name: 'Admin User',
  permissions: ['read', 'write', 'delete']
};

const admin2: AdminUserType = {
  id: 'admin-2',
  name: 'Admin User',
  permissions: ['read', 'write', 'delete']
};
3. Type Aliases Can Represent Anything:

// Type aliases work with primitives, unions, tuples
type StringOrNumber = string | number;
type Coordinates = [number, number];
type Callback = () => void;

// Interfaces are limited to object shapes
// interface StringOrNumber = string | number; // Error!
// interface Coordinates = [number, number]; // Error!

// However, interfaces can represent functions
interface CallbackInterface {
  (): void;
}

// Both work for function types
const callback1: Callback = () => console.log('Called');
const callback2: CallbackInterface = () => console.log('Called');
Warning: Be careful with declaration merging. While it's useful for extending third-party library types, accidental merging can lead to unexpected behavior. Type aliases prevent this by disallowing duplicates.

When to Use Type Aliases vs Interfaces

Here are practical guidelines for choosing between type aliases and interfaces:

Use Interfaces When:

// 1. Defining object shapes (especially public APIs)
interface ApiResponse {
  data: unknown;
  status: number;
  message: string;
}

// 2. You might need declaration merging
interface CustomWindow {
  myCustomProperty: string;
}

// 3. Working with classes (interfaces work naturally with implements)
interface Drawable {
  draw(): void;
}

class Circle implements Drawable {
  draw() {
    console.log('Drawing circle');
  }
}

// 4. Building extensible type hierarchies
interface Vehicle {
  brand: string;
  model: string;
}

interface Car extends Vehicle {
  doors: number;
}

interface ElectricCar extends Car {
  batteryCapacity: number;
}
Use Type Aliases When:

// 1. Defining union types
type Result = 'success' | 'error' | 'pending';

// 2. Defining tuple types
type RGB = [number, number, number];

// 3. Complex type manipulations
type Nullable<T> = T | null;
type ReadonlyProps<T> = {
  readonly [K in keyof T]: T[K];
};

// 4. Function types
type EventHandler = (event: Event) => void;

// 5. Mapped types and utility types
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// 6. Intersection types
type UserWithTimestamps = User & Timestamped & Identifiable;
Tip: Many developers prefer interfaces for object shapes and type aliases for everything else. This is a good default strategy, but use what works best for your project's needs.

Combining Type Aliases and Interfaces

You can mix and match type aliases and interfaces in the same codebase:

Hybrid Approach:

// Type alias for union
type UserRole = 'admin' | 'editor' | 'viewer';

// Interface using type alias
interface UserAccount {
  id: string;
  username: string;
  role: UserRole; // Using type alias
  metadata: UserMetadata;
}

// Type alias using interface
type UserMetadata = {
  lastLogin: Date;
  loginCount: number;
};

// Interface extending with intersection
interface PremiumUser extends UserAccount {
  subscriptionLevel: 'basic' | 'pro' | 'enterprise';
}

// Type alias combining interface and type
type FullUserProfile = UserAccount & {
  preferences: {
    theme: 'light' | 'dark';
    language: string;
  };
  notifications: boolean;
};

// Implementation
const premiumUser: PremiumUser = {
  id: 'user-001',
  username: 'john_doe',
  role: 'admin',
  subscriptionLevel: 'pro',
  metadata: {
    lastLogin: new Date(),
    loginCount: 150
  }
};

const fullProfile: FullUserProfile = {
  id: 'user-002',
  username: 'jane_smith',
  role: 'editor',
  metadata: {
    lastLogin: new Date(),
    loginCount: 75
  },
  preferences: {
    theme: 'dark',
    language: 'en'
  },
  notifications: true
};
Exercise:
  1. Create an interface Book with properties: title, author, isbn (readonly), and optional publishedYear
  2. Create a type alias BookFormat as a union of 'hardcover', 'paperback', 'ebook', and 'audiobook'
  3. Create an interface PhysicalBook that extends Book and adds format: BookFormat and pageCount: number
  4. Create a type alias DigitalBook using intersection that combines Book with an object containing fileSize: number and format: 'ebook' | 'audiobook'
  5. Create instances of both PhysicalBook and DigitalBook with sample data

Summary

  • Type aliases use the type keyword and can represent any type (primitives, unions, tuples, objects, functions)
  • Interfaces use the interface keyword and are specialized for object shapes
  • Interfaces support extension with the extends keyword and declaration merging
  • Type aliases support intersections with the & operator for combining types
  • Choose interfaces for object shapes, public APIs, and extensible hierarchies
  • Choose type aliases for unions, tuples, complex type manipulations, and function types
  • Both can be combined in the same codebase for maximum flexibility