Dev Logs
/TypeScript/ Interfaces and Type Aliases
Chapters
  • 01Introduction to TypeScript
  • 02TypeScript Setup and Configuration
  • 03Basic Type Annotations
  • 04Interfaces and Type Aliases
    • What are Interfaces?
    • Basic Interface Syntax
    • Simple Interface
    • Optional Properties
    • Readonly Properties
    • Interface Extension
    • Basic Extension
    • Multiple Extension
    • Function Types in Interfaces
    • Method Signatures
    • Event Handler Interfaces
    • Index Signatures
    • String Index Signatures
    • Number Index Signatures
    • Mixed Index Signatures
    • Type Aliases
    • Basic Type Aliases
    • Object Type Aliases
    • Union Types
    • Function Type Aliases
    • Intersection Types
    • Interface vs Type Alias: When to Use Which?
    • Use Interfaces When:
    • Use Type Aliases When:
    • Advanced Interface Patterns
    • Generic Interfaces
    • Conditional Properties
    • Best Practices
    • Good Practices
    • Avoid
    • Next Steps
  • 05Functions and Type Safety
  • 06Arrays, Tuples, and Enums
  • 07Union and Intersection Types
  • 08Type Inference and Narrowing
  • 09Optional and Readonly Properties
  • 10Classes and Object-Oriented Programming
  • 11Generics and Reusable Code
  • 12Type Guards and Advanced Type Checking
  • 13Advanced TypeScript Features
  • 14Modules and Namespaces
  • 15Error Handling in TypeScript
  • 16Utility Types and Type Manipulations
  • 17Declaration Merging and Ambient Declarations
All chapters

Interfaces and Type Aliases

Learn how to define custom types using interfaces and type aliases, and understand when to use each

What are Interfaces?

Interfaces define the structure of objects, specifying what properties and methods an object should have. They act as contracts that ensure objects conform to a specific shape.

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// Object must match the interface
const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
};

Basic Interface Syntax

Simple Interface

typescript
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

function displayProduct(product: Product): void {
  console.log(`${product.name}: $${product.price}`);
}

const laptop: Product = {
  id: 1,
  name: "MacBook Pro",
  price: 1999,
  description: "Powerful laptop for professionals",
};

displayProduct(laptop);

Optional Properties

typescript
interface UserProfile {
  id: number;
  username: string;
  email: string;
  avatar?: string; // Optional property
  bio?: string;
  website?: string;
}

// Valid - optional properties can be omitted
const user1: UserProfile = {
  id: 1,
  username: "johndoe",
  email: "john@example.com",
};

// Also valid - optional properties included
const user2: UserProfile = {
  id: 2,
  username: "janedoe",
  email: "jane@example.com",
  avatar: "avatar.jpg",
  bio: "Software developer",
};

Readonly Properties

typescript
interface Config {
  readonly apiUrl: string;
  readonly version: string;
  timeout: number; // Can be modified
}

const appConfig: Config = {
  apiUrl: "https://api.example.com",
  version: "1.0.0",
  timeout: 5000,
};

// appConfig.apiUrl = "new-url"; // Error: Cannot assign to 'apiUrl' because it is a read-only property
appConfig.timeout = 10000; // OK: timeout is not readonly

Interface Extension

Basic Extension

typescript
interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

interface Cat extends Animal {
  color: string;
  meow(): void;
}

const myDog: Dog = {
  name: "Buddy",
  age: 3,
  breed: "Golden Retriever",
  bark() {
    console.log("Woof!");
  },
};

Multiple Extension

typescript
interface Flyable {
  fly(): void;
  altitude: number;
}

interface Swimmable {
  swim(): void;
  depth: number;
}

// Extending multiple interfaces
interface Duck extends Animal, Flyable, Swimmable {
  quack(): void;
}

const duck: Duck = {
  name: "Donald",
  age: 2,
  altitude: 0,
  depth: 0,
  fly() {
    this.altitude = 100;
    console.log("Flying!");
  },
  swim() {
    this.depth = 5;
    console.log("Swimming!");
  },
  quack() {
    console.log("Quack!");
  },
};

Function Types in Interfaces

Method Signatures

typescript
interface Calculator {
  // Method signature
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;

  // Property with function type
  multiply: (a: number, b: number) => number;

  // Optional method
  divide?(a: number, b: number): number;
}

const calc: Calculator = {
  add(a, b) {
    return a + b;
  },
  subtract(a, b) {
    return a - b;
  },
  multiply: (a, b) => a * b,
  // divide is optional, so we can omit it
};

Event Handler Interfaces

typescript
interface EventHandler {
  onClick(event: MouseEvent): void;
  onKeyPress(event: KeyboardEvent): void;
  onSubmit?(event: SubmitEvent): void;
}

interface ButtonProps {
  text: string;
  disabled?: boolean;
  handler: EventHandler;
}

function createButton(props: ButtonProps): HTMLButtonElement {
  const button = document.createElement("button");
  button.textContent = props.text;
  button.disabled = props.disabled || false;

  button.addEventListener("click", props.handler.onClick);
  button.addEventListener("keypress", props.handler.onKeyPress);

  return button;
}

Index Signatures

String Index Signatures

typescript
interface StringDictionary {
  [key: string]: string;
}

const translations: StringDictionary = {
  hello: "Hola",
  goodbye: "Adiós",
  thanks: "Gracias",
};

// Can add any string key
translations.welcome = "Bienvenido";

Number Index Signatures

typescript
interface NumberArray {
  [index: number]: number;
  length: number; // Can have named properties too
}

const scores: NumberArray = {
  0: 95,
  1: 87,
  2: 92,
  length: 3,
};

Mixed Index Signatures

typescript
interface MixedDictionary {
  [key: string]: string | number;
  name: string; // Must be compatible with index signature
  age: number;
}

const person: MixedDictionary = {
  name: "John",
  age: 30,
  city: "New York",
  zipCode: 10001,
};

Type Aliases

Type aliases create new names for existing types, including complex type combinations.

Basic Type Aliases

typescript
// Primitive type alias
type UserID = string;
type Age = number;
type IsActive = boolean;

// Using type aliases
function getUser(id: UserID): User | null {
  // Implementation
  return null;
}

function updateAge(userId: UserID, newAge: Age): void {
  // Implementation
}

Object Type Aliases

typescript
type Point = {
  x: number;
  y: number;
};

type Rectangle = {
  topLeft: Point;
  bottomRight: Point;
};

function calculateArea(rect: Rectangle): number {
  const width = rect.bottomRight.x - rect.topLeft.x;
  const height = rect.bottomRight.y - rect.topLeft.y;
  return width * height;
}

Union Types

typescript
type Status = "pending" | "approved" | "rejected" | "cancelled";
type Theme = "light" | "dark" | "auto";
type Size = "small" | "medium" | "large";

// Union of different types
type ID = string | number;
type Response = string | { error: string } | { data: any };

function handleResponse(response: Response): void {
  if (typeof response === "string") {
    console.log("Message:", response);
  } else if ("error" in response) {
    console.error("Error:", response.error);
  } else {
    console.log("Data:", response.data);
  }
}

Function Type Aliases

typescript
// Function type aliases
type EventCallback = (event: Event) => void;
type Validator<T> = (value: T) => boolean;
type Transformer<T, U> = (input: T) => U;

// Using function type aliases
const emailValidator: Validator<string> = (email) => {
  return email.includes("@");
};

const stringToNumber: Transformer<string, number> = (str) => {
  return parseInt(str, 10);
};

function addEventListener(
  element: HTMLElement,
  event: string,
  callback: EventCallback
): void {
  element.addEventListener(event, callback);
}

Intersection Types

typescript
type Name = {
  firstName: string;
  lastName: string;
};

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

type Address = {
  street: string;
  city: string;
  zipCode: string;
};

// Intersection type - combines all properties
type Person = Name & Contact & Address;

const person: Person = {
  firstName: "John",
  lastName: "Doe",
  email: "john@example.com",
  phone: "555-1234",
  street: "123 Main St",
  city: "Anytown",
  zipCode: "12345",
};

// Intersection with interfaces
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

type UserWithTimestamp = User & Timestamped;

Interface vs Type Alias: When to Use Which?

Use Interfaces When:

typescript
// 1. Defining object shapes
interface User {
  id: number;
  name: string;
}

// 2. You need declaration merging
interface Window {
  customProperty: string;
}

// Later in another file...
interface Window {
  anotherProperty: number;
}
// Window now has both properties

// 3. Extending classes
class BaseUser implements User {
  constructor(public id: number, public name: string) {}
}

// 4. When you might extend later
interface AdminUser extends User {
  permissions: string[];
}

Use Type Aliases When:

typescript
// 1. Union types
type Status = "loading" | "success" | "error";

// 2. Intersection types
type UserWithRole = User & { role: string };

// 3. Computed properties
type Keys = "name" | "email";
type UserSubset = {
  [K in Keys]: string;
};

// 4. Complex type manipulations
type Optional<T> = {
  [K in keyof T]?: T[K];
};

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

// 6. Primitive aliases
type UserID = string;

Advanced Interface Patterns

Generic Interfaces

typescript
interface Repository<T> {
  findById(id: string): T | null;
  save(entity: T): T;
  delete(id: string): boolean;
  findAll(): T[];
}

interface User {
  id: string;
  name: string;
  email: string;
}

class UserRepository implements Repository<User> {
  private users: User[] = [];

  findById(id: string): User | null {
    return this.users.find((user) => user.id === id) || null;
  }

  save(user: User): User {
    this.users.push(user);
    return user;
  }

  delete(id: string): boolean {
    const index = this.users.findIndex((user) => user.id === id);
    if (index > -1) {
      this.users.splice(index, 1);
      return true;
    }
    return false;
  }

  findAll(): User[] {
    return [...this.users];
  }
}

Conditional Properties

typescript
interface BaseConfig {
  apiUrl: string;
  timeout: number;
}

interface DevConfig extends BaseConfig {
  debug: true;
  logLevel: "verbose" | "info" | "warn" | "error";
}

interface ProdConfig extends BaseConfig {
  debug: false;
  compression: boolean;
}

type Config = DevConfig | ProdConfig;

function createLogger(config: Config): void {
  if (config.debug) {
    // TypeScript knows this is DevConfig
    console.log(`Log level: ${config.logLevel}`);
  } else {
    // TypeScript knows this is ProdConfig
    console.log(`Compression: ${config.compression}`);
  }
}

Best Practices

✅ Good Practices

typescript
// Use descriptive names
interface UserAccount {
  id: string;
  email: string;
}

// Group related properties
interface UserProfile {
  personal: {
    firstName: string;
    lastName: string;
    dateOfBirth: Date;
  };
  contact: {
    email: string;
    phone?: string;
  };
  preferences: {
    theme: "light" | "dark";
    notifications: boolean;
  };
}

// Use readonly for immutable data
interface ApiResponse {
  readonly data: any;
  readonly status: number;
  readonly timestamp: Date;
}

❌ Avoid

typescript
// Don't use overly generic names
interface Data {
  value: any;
}

// Don't make everything optional
interface BadUser {
  id?: string;
  name?: string;
  email?: string;
}

// Don't use any when you can be specific
interface BadResponse {
  data: any; // Be more specific about the data structure
}

Next Steps

Now that you understand interfaces and type aliases, let's explore how to add types to functions, including parameters, return types, and overloads.


Continue to: Functions and Type Safety

PreviousBasic Type AnnotationsNextFunctions and Type Safety

Open source, free forever. Built by iammhador.

Contribute on GitHub