You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

TypeScript Interfaces

Define contracts for object shapes, ensuring consistency and type safety across your application.

TypeScript Interfaces

Master TypeScript interfaces with free flashcards and spaced repetition practice. This lesson covers interface syntax, optional properties, readonly modifiers, extending interfaces, and function type definitionsβ€”essential concepts for building type-safe TypeScript applications.

Welcome to TypeScript Interfaces πŸ’»

Interfaces are one of TypeScript's most powerful features for defining contracts within your code. They describe the shape of objects, ensuring that your data structures adhere to specific patterns. Think of interfaces as blueprints that tell TypeScript exactly what properties and methods an object should have.

While JavaScript is dynamically typed, TypeScript's interfaces bring static type checking to your development workflow, catching errors before your code runs. This dramatically improves code quality, maintainability, and developer experience.

Core Concepts

What Are Interfaces?

An interface in TypeScript is a syntactical contract that defines the structure of an object. It specifies:

  • Property names and their types
  • Optional vs. required properties
  • Readonly properties that can't be modified
  • Method signatures
  • Index signatures for dynamic property names

Interfaces exist only at compile timeβ€”they're completely removed from the final JavaScript output. They're purely for type checking during development.

πŸ’‘ Key Insight: Interfaces describe what an object looks like, not how it behaves. They define the contract without implementation.

Basic Interface Syntax

Here's the fundamental structure:

interface User {
  name: string;
  age: number;
  email: string;
}

This interface defines a User type that must have three properties: name (string), age (number), and email (string). Any object claiming to be a User must have all three.

Using the interface:

const user: User = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

If you try to create a User without one of these properties, or with the wrong type, TypeScript will raise a compile-time error:

// ❌ Error: Property 'email' is missing
const invalidUser: User = {
  name: "Bob",
  age: 25
};

Optional Properties

Not every property needs to be required. Use the ? modifier to make properties optional:

interface Product {
  id: number;
  name: string;
  description?: string;  // Optional
  price: number;
  discount?: number;     // Optional
}

Optional properties can be omitted when creating objects:

const product1: Product = {
  id: 1,
  name: "Laptop",
  price: 999
  // description and discount are optional
};

const product2: Product = {
  id: 2,
  name: "Mouse",
  description: "Wireless mouse",
  price: 29,
  discount: 5
};

⚠️ Important: When accessing optional properties, TypeScript knows they might be undefined, so you should check before using them:

if (product1.description) {
  console.log(product1.description.toUpperCase());
}

Readonly Properties

The readonly modifier prevents properties from being changed after object creation:

interface Config {
  readonly apiKey: string;
  readonly baseUrl: string;
  timeout: number;  // Can be modified
}

const config: Config = {
  apiKey: "abc123",
  baseUrl: "https://api.example.com",
  timeout: 5000
};

config.timeout = 10000;  // βœ… OK
// config.apiKey = "new-key";  // ❌ Error: Cannot assign to 'apiKey'

πŸ’‘ Use Case: Use readonly for properties that should be set once during initialization and never changed, like configuration values, IDs, or timestamps.

Function Type Interfaces

Interfaces can describe function signatures, not just object shapes:

interface MathOperation {
  (x: number, y: number): number;
}

const add: MathOperation = (x, y) => x + y;
const multiply: MathOperation = (x, y) => x * y;

You can also define methods within object interfaces:

interface Calculator {
  add(x: number, y: number): number;
  subtract(x: number, y: number): number;
  reset(): void;
}

const calc: Calculator = {
  add(x, y) { return x + y; },
  subtract(x, y) { return x - y; },
  reset() { console.log("Reset"); }
};

Extending Interfaces

Interfaces can extend other interfaces, inheriting their properties. This promotes code reuse and creates hierarchies:

interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: number;
  department: string;
}

const employee: Employee = {
  name: "John",
  age: 35,
  employeeId: 12345,
  department: "Engineering"
};

You can extend multiple interfaces:

interface Printable {
  print(): void;
}

interface Saveable {
  save(): void;
}

interface Document extends Printable, Saveable {
  title: string;
  content: string;
}

🧠 Mental Model: Think of interface extension like inheritance in object-oriented programming, but for type definitions rather than implementations.

Index Signatures

When you don't know all property names ahead of time, use index signatures:

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

const translations: StringDictionary = {
  hello: "hola",
  goodbye: "adiΓ³s",
  thanks: "gracias"
};

You can combine index signatures with known properties:

interface ApiResponse {
  status: number;
  message: string;
  [key: string]: any;  // Additional dynamic properties
}

const response: ApiResponse = {
  status: 200,
  message: "Success",
  data: { userId: 1 },
  timestamp: 1234567890
};

⚠️ Caution: Index signatures with any reduce type safety. Use them sparingly and consider more specific types when possible.

Detailed Examples

Example 1: Building a User Profile System πŸ‘€

Let's create a comprehensive user profile system with multiple interface types:

interface Address {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
}

interface ContactInfo {
  email: string;
  phone?: string;
  address?: Address;
}

interface UserProfile extends ContactInfo {
  readonly id: number;
  username: string;
  firstName: string;
  lastName: string;
  dateOfBirth: Date;
  isActive: boolean;
}

interface AdminProfile extends UserProfile {
  permissions: string[];
  accessLevel: number;
}

// Creating instances
const regularUser: UserProfile = {
  id: 1001,
  username: "alice_wonder",
  firstName: "Alice",
  lastName: "Wonder",
  email: "alice@example.com",
  dateOfBirth: new Date("1990-05-15"),
  isActive: true
};

const admin: AdminProfile = {
  id: 5001,
  username: "admin_bob",
  firstName: "Bob",
  lastName: "Admin",
  email: "bob@example.com",
  phone: "555-0123",
  dateOfBirth: new Date("1985-03-20"),
  isActive: true,
  permissions: ["read", "write", "delete"],
  accessLevel: 10
};

Why this works well:

  • Separation of concerns: Each interface handles a specific aspect (address, contact, user, admin)
  • Reusability: Address and ContactInfo can be used in other contexts
  • Type safety: The readonly id prevents accidental ID changes
  • Hierarchy: AdminProfile naturally extends UserProfile with additional admin-specific fields

Example 2: API Response Handling 🌐

Interfaces excel at defining API contracts:

interface ApiError {
  code: string;
  message: string;
  details?: string[];
}

interface PaginationInfo {
  currentPage: number;
  totalPages: number;
  itemsPerPage: number;
  totalItems: number;
}

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: ApiError;
  pagination?: PaginationInfo;
  timestamp: number;
}

interface BlogPost {
  id: number;
  title: string;
  author: string;
  content: string;
  publishedAt: Date;
  tags: string[];
}

// Type-safe API responses
const successResponse: ApiResponse<BlogPost[]> = {
  success: true,
  data: [
    {
      id: 1,
      title: "Getting Started with TypeScript",
      author: "Jane Doe",
      content: "TypeScript is a powerful...",
      publishedAt: new Date(),
      tags: ["typescript", "programming"]
    }
  ],
  pagination: {
    currentPage: 1,
    totalPages: 10,
    itemsPerPage: 10,
    totalItems: 100
  },
  timestamp: Date.now()
};

const errorResponse: ApiResponse<null> = {
  success: false,
  error: {
    code: "NOT_FOUND",
    message: "Resource not found",
    details: ["The requested blog post does not exist"]
  },
  timestamp: Date.now()
};

Key benefits:

  • Generic type parameter <T> makes ApiResponse reusable for any data type
  • Discriminated union: The success field helps TypeScript narrow types
  • Comprehensive error handling: Structured error information
  • Consistent structure: All API responses follow the same pattern

Example 3: Event System with Function Types 🎯

Interfaces are perfect for defining event handlers and callback systems:

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

interface KeyboardEvent {
  key: string;
  ctrlKey: boolean;
  shiftKey: boolean;
  altKey: boolean;
}

interface EventHandler<T> {
  (event: T): void;
}

interface EventEmitter {
  on(eventName: string, handler: EventHandler<any>): void;
  off(eventName: string, handler: EventHandler<any>): void;
  emit(eventName: string, data: any): void;
}

class SimpleEventEmitter implements EventEmitter {
  private handlers: Map<string, EventHandler<any>[]> = new Map();

  on(eventName: string, handler: EventHandler<any>): void {
    if (!this.handlers.has(eventName)) {
      this.handlers.set(eventName, []);
    }
    this.handlers.get(eventName)!.push(handler);
  }

  off(eventName: string, handler: EventHandler<any>): void {
    const eventHandlers = this.handlers.get(eventName);
    if (eventHandlers) {
      const index = eventHandlers.indexOf(handler);
      if (index > -1) {
        eventHandlers.splice(index, 1);
      }
    }
  }

  emit(eventName: string, data: any): void {
    const eventHandlers = this.handlers.get(eventName);
    if (eventHandlers) {
      eventHandlers.forEach(handler => handler(data));
    }
  }
}

// Usage
const emitter = new SimpleEventEmitter();

const handleMouseClick: EventHandler<MousePosition> = (pos) => {
  console.log(`Clicked at (${pos.x}, ${pos.y})`);
};

const handleKeyPress: EventHandler<KeyboardEvent> = (event) => {
  console.log(`Key pressed: ${event.key}`);
};

emitter.on("click", handleMouseClick);
emitter.on("keypress", handleKeyPress);

emitter.emit("click", { x: 100, y: 200 });
emitter.emit("keypress", { key: "Enter", ctrlKey: false, shiftKey: false, altKey: false });

Design highlights:

  • Generic event handlers: EventHandler<T> works with any event type
  • Clean API: The EventEmitter interface defines a clear contract
  • Type safety: Each event handler knows exactly what data structure it receives
  • Implementation flexibility: Any class can implement EventEmitter

Example 4: Configuration System with Readonly πŸ”§

Interfaces with readonly properties ensure configuration immutability:

interface DatabaseConfig {
  readonly host: string;
  readonly port: number;
  readonly username: string;
  readonly password: string;
  readonly database: string;
  readonly ssl: boolean;
}

interface ServerConfig {
  readonly port: number;
  readonly hostname: string;
  readonly environment: "development" | "staging" | "production";
}

interface AppConfig {
  readonly database: DatabaseConfig;
  readonly server: ServerConfig;
  readonly apiKeys: Readonly<{
    stripe: string;
    sendgrid: string;
    aws: string;
  }>;
  readonly features: Readonly<{
    enableAnalytics: boolean;
    enableBetaFeatures: boolean;
  }>;
}

const config: AppConfig = {
  database: {
    host: "localhost",
    port: 5432,
    username: "dbuser",
    password: "secure_password",
    database: "myapp_db",
    ssl: true
  },
  server: {
    port: 3000,
    hostname: "localhost",
    environment: "development"
  },
  apiKeys: {
    stripe: "sk_test_...",
    sendgrid: "SG....",
    aws: "AKIA..."
  },
  features: {
    enableAnalytics: false,
    enableBetaFeatures: true
  }
};

// ❌ These would cause errors:
// config.database.host = "newhost"; // Cannot assign to 'host'
// config.server.port = 4000; // Cannot assign to 'port'
// config.apiKeys.stripe = "new-key"; // Cannot assign to 'stripe'

Benefits of this approach:

  • Immutability: Configuration can't be accidentally modified at runtime
  • Clarity: Developers know these values are constants
  • Nested readonly: Using Readonly<> utility type makes nested objects immutable too
  • Type safety: Environment is restricted to specific string literals

Common Mistakes ⚠️

Mistake 1: Confusing Interfaces with Types

While interfaces and type aliases are similar, they have key differences:

// Interface (can be extended and merged)
interface User {
  name: string;
}

interface User {
  age: number;  // βœ… Declaration merging works
}

// Type alias (cannot be merged)
type Product = {
  name: string;
};

// type Product = { // ❌ Error: Duplicate identifier
//   price: number;
// };

πŸ’‘ Rule of thumb: Use interfaces for object shapes that might be extended. Use type aliases for unions, primitives, tuples, or when you need advanced type features.

Mistake 2: Forgetting Optional Property Checks

interface Config {
  timeout?: number;
}

const config: Config = {};

// ❌ Dangerous - might be undefined
const doubled = config.timeout * 2;

// βœ… Safe approaches
const doubled1 = (config.timeout ?? 5000) * 2;  // Nullish coalescing
const doubled2 = config.timeout ? config.timeout * 2 : 0;  // Conditional

Mistake 3: Over-Using any in Index Signatures

// ❌ Too permissive
interface DataStore {
  [key: string]: any;
}

// βœ… More specific
interface DataStore {
  [key: string]: string | number | boolean;
}

// βœ… Even better - use known properties when possible
interface DataStore {
  name: string;
  age: number;
  active: boolean;
  [key: string]: string | number | boolean;  // Only for additional props
}

Mistake 4: Readonly Shallow Understanding

interface Config {
  readonly settings: {
    theme: string;
  };
}

const config: Config = {
  settings: { theme: "dark" }
};

// config.settings = { theme: "light" }; // ❌ Error: readonly
config.settings.theme = "light"; // βœ… This works! readonly is shallow

Solution: Use Readonly<> utility type for deep immutability:

interface Config {
  readonly settings: Readonly<{
    theme: string;
  }>;
}

Mistake 5: Not Using Interfaces for Function Parameters

// ❌ Inline object types are less maintainable
function createUser(user: { name: string; email: string; age: number }) {
  // ...
}

// βœ… Interface provides better documentation and reusability
interface CreateUserParams {
  name: string;
  email: string;
  age: number;
}

function createUser(user: CreateUserParams) {
  // ...
}

Mistake 6: Excessive Interface Nesting

// ❌ Overly complex nesting
interface Company {
  info: {
    details: {
      contact: {
        address: {
          street: string;
        };
      };
    };
  };
}

// βœ… Flatten with separate interfaces
interface Address {
  street: string;
}

interface Contact {
  address: Address;
}

interface Company {
  contact: Contact;
}

Key Takeaways 🎯

βœ… Interfaces define contracts for object shapes, ensuring type safety

βœ… Use ? for optional properties and readonly for immutable properties

βœ… Extend interfaces to create hierarchies and promote code reuse

βœ… Generic interfaces (Interface<T>) provide flexibility while maintaining type safety

βœ… Index signatures allow dynamic property names with type constraints

βœ… Interfaces exist only at compile time and add zero runtime overhead

βœ… Prefer interfaces over inline types for better maintainability and documentation

βœ… Always check optional properties before using them to avoid runtime errors

βœ… Use Readonly<> utility type for deep immutability of nested objects

βœ… Interfaces can describe functions, not just object structures

πŸ“‹ Quick Reference Card

πŸ“‹ TypeScript Interface Cheat Sheet

Basic Interfaceinterface User { name: string; }
Optional Propertyage?: number;
Readonly Propertyreadonly id: number;
Extend Interfaceinterface Admin extends User { ... }
Function Typeinterface Fn { (x: number): string; }
Method Signaturesave(): void;
Index Signature[key: string]: any;
Generic Interfaceinterface Box<T> { value: T; }
Readonly UtilityReadonly<{ x: number }>
Multiple Extendsinterface A extends B, C { ... }

πŸ“š Further Study

  1. TypeScript Handbook - Interfaces: https://www.typescriptlang.org/docs/handbook/interfaces.html
  2. TypeScript Deep Dive - Interfaces: https://basarat.gitbook.io/typescript/type-system/interfaces
  3. Advanced Interface Patterns: https://www.typescriptlang.org/docs/handbook/2/objects.html

Ready to practice? The quiz below will test your understanding of TypeScript interfaces with hands-on code examples. Remember: interfaces are about defining contractsβ€”they tell TypeScript what shape your data should have, ensuring type safety throughout your application! πŸ’ͺ