Skip to content

TypeScript Interfaces — Complete Guide with Examples

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Interfaces. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript interfaces define the shape of objects — they act as contracts that specify which properties and methods an object must have, making your code self-documenting and catching structural mismatches before runtime.

What You'll Learn

  • Interface syntax and basic usage
  • Optional and readonly properties
  • Index signatures for dynamic keys
  • Extending interfaces (inheritance)
  • Real-world interface patterns

Why It Matters

Without interfaces, every object in TypeScript is an anonymous shape. Interfaces give names to these shapes, making code easier to reason about, refactor, and share. They are the primary tool for defining contracts between different parts of your application — between functions, between modules, and between your code and external APIs.

Real-World Use

The Doda Browser extension API uses interfaces to define every message that passes between the extension and the browser. The TabMessage interface ensures every tab event includes tabId, url, and title — no more "undefined is not an object" errors when handling tab updates.

Learning Path

flowchart LR
  A[Basic Types] --> B[Interfaces]
  B --> C[Type Aliases]
  B --> D[You Are Here]
  C --> E[Functions]
  E --> F[Enums]
  F --> G[Generics Basics]

Interface Basics

An interface describes the shape of an object:

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

function registerUser(user: User): void {
  console.log(`Registered ${user.name} (${user.email})`);
}

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

registerUser(newUser); // OK

// Missing properties:
// registerUser({ name: "Bob" }); // Error: property 'age' missing
// Extra properties:
// registerUser({ name: "Bob", age: 25, email: "b@e.com", extra: true }); // Error

Think of an interface like a job posting: it lists the requirements. Any object applying for the job must meet them exactly.

Optional Properties

Use ? to mark properties as optional:

interface UserProfile {
  username: string;
  bio?: string;        // Optional
  avatarUrl?: string;  // Optional
  joinedAt: Date;
}

const profile1: UserProfile = {
  username: "alice99",
  joinedAt: new Date("2024-01-15"),
};

const profile2: UserProfile = {
  username: "bob_dev",
  bio: "Full-stack developer",
  joinedAt: new Date("2024-03-20"),
};

When accessing an optional property, TypeScript reminds you it might be undefined:

// profile1.bio.toUpperCase(); // Error: Object is possibly 'undefined'
if (profile1.bio) {
  console.log(profile1.bio.toUpperCase()); // OK — narrowed
}
// Or use optional chaining:
console.log(profile1.bio?.toUpperCase()); // undefined if missing

Readonly Properties

Mark properties that should never change after creation:

interface Config {
  readonly apiKey: string;
  readonly endpoint: string;
  timeout: number; // Can change
}

const appConfig: Config = {
  apiKey: "sk-abc123",
  endpoint: "https://api.example.com",
  timeout: 5000,
};

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

Use readonly for configuration, IDs, timestamps, and any value that should be immutable after initialization.

Index Signatures

When you don't know the property names in advance but know the value types:

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

const env: StringMap = {
  NODE_ENV: "production",
  API_KEY: "sk-abc",
  DB_HOST: "localhost",
};

// env.PORT = 3000; // Error: Type 'number' is not assignable to type 'string'

Index signatures are common for:

  • Dictionary/map-like objects
  • Environment variables
  • HTTP headers
  • Caching layers

Mixing Named and Index Properties

interface HttpResponse {
  status: number;
  headers: { [key: string]: string };
  body: unknown;
}

const response: HttpResponse = {
  status: 200,
  headers: {
    "Content-Type": "application/json",
    "X-Request-Id": "req-abc",
  },
  body: { userId: 1 },
};

Interface Extension (extends)

Interfaces can extend one or more other interfaces:

interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  name: string;
  email: string;
  role: "admin" | "user";
}

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

const admin: AdminUser = {
  id: "usr-001",
  createdAt: new Date("2024-01-01"),
  updatedAt: new Date("2024-06-01"),
  name: "Alice",
  email: "alice@example.com",
  role: "admin",
  permissions: ["read", "write", "delete"],
};

Multiple Inheritance

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface SoftDeletable {
  deletedAt?: Date;
  deletedBy?: string;
}

interface Product extends Timestamped, SoftDeletable {
  id: string;
  name: string;
  price: number;
}

Interface vs Type Alias

Both interfaces and type aliases can describe object shapes, but there are differences:

Feature Interface Type Alias
Extension extends & (intersection)
Declaration merging Yes No
Can describe unions/tuples No Yes
Performance Better (cached) Slightly slower
// Interface — can be extended later
interface Animal {
  name: string;
}

// Declaration merging (interfaces only)
interface Animal {
  age: number;
}
const pet: Animal = { name: "Rex", age: 3 }; // Combines both declarations

Rule of thumb: Use interface for public API object shapes. Use type for unions, intersections, and computed types.

Common Mistakes

1. Forgetting Optional Property Syntax

interface User {
  name: string;
  nickname: string | undefined; // Wrong: still required, just can be undefined
  bio?: string; // Correct: completely optional
}

2. Expecting Excess Property Checking in Variables

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

const p = { x: 10, y: 20, z: 30 };
const point: Point = p; // No error — excess property checking only works for object literals

3. Confusing readonly in Interface vs const

readonly in an interface prevents reassignment of that property. It does not make the value deeply immutable.

interface Config {
  readonly tags: string[];
}
const cfg: Config = { tags: ["a"] };
cfg.tags.push("b"); // OK — readonly prevents reassignment, not mutation

4. Overusing Index Signatures

interface Loose {
  [key: string]: any; // Too loose — defeats purpose
}

Instead, be specific with known properties and use index signatures only for genuinely dynamic data.

5. Not Extending When Interfaces Share Fields

Don't repeat yourself:

// Bad
interface Cat { name: string; breed: string; meow(): void; }
interface Dog { name: string; breed: string; bark(): void; }

// Good
interface Pet { name: string; breed: string; }
interface Cat extends Pet { meow(): void; }
interface Dog extends Pet { bark(): void; }

6. Using Interface for Union Types

Interfaces cannot represent unions. Use type instead:

// interface Status ="active" | "inactive"; // Error
type Status = "active" | "inactive"; // OK

Practice Questions

  1. Can an interface extend multiple interfaces? Yes: interface C extends A, B { ... }

  2. What is the difference between readonly in an interface and const? readonly applies to a property (cannot reassign), while const applies to a variable. readonly does not prevent mutation of arrays/objects, just reassignment.

  3. When would you use an index signature? When you have an object where property names are dynamic, like environment variables, HTTP headers, or a dictionary.

  4. What happens if you define the same interface twice? TypeScript merges them. Both declarations are combined into a single interface with all properties.

Challenge: Define interfaces for a blog system: Post, Comment, Author. Post extends BaseEntity. Comment has a postId reference. Include optional and readonly properties where appropriate.

FAQ

What is the difference between `interface` and `type` in TypeScript?

interface can be extended via extends and supports declaration merging. type can represent unions, intersections, and computed types. Use interface for object shapes that may be extended; use type for everything else.

Can I use a class as an interface?

Yes. A class definition creates both a value (constructor) and a type (instance shape). You can use a class where an interface is expected: function greet(user: UserClass) { ... }

What is declaration merging?

When you define the same interface multiple times, TypeScript merges them into one. This is useful for extending types from third-party libraries.

Can interfaces have default values?

No. Interfaces are purely compile-time constructs. Default values are a runtime concept and belong in function parameters or class constructors.

How do I make a property truly immutable?

Use readonly in the interface and Object.freeze() at runtime for the actual value.

Mini Project: Configuration Manager

Build a typed configuration system using interfaces:

// src/config-manager.ts

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

interface CacheConfig {
  provider: "redis" | "memory";
  ttlSeconds: number;
  host?: string;
  port?: number;
}

interface AppConfig {
  appName: string;
  version: string;
  debug: boolean;
  database: DatabaseConfig;
  cache: CacheConfig;
  features: { [key: string]: boolean };
}

const config: AppConfig = {
  appName: "DodaTech Scanner",
  version: "2.1.0",
  debug: false,
  database: {
    host: "localhost",
    port: 5432,
    username: "admin",
    password: "secret",
    database: "scanner_db",
    ssl: true,
    poolSize: 10,
  },
  cache: {
    provider: "redis",
    ttlSeconds: 3600,
    host: "redis.internal",
    port: 6379,
  },
  features: {
    realTimeScanning: true,
    autoUpdate: true,
    betaFeatures: false,
  },
};

function printConfig(config: AppConfig): void {
  console.log(`App: ${config.appName} v${config.version}`);
  console.log(`Database: ${config.database.host}:${config.database.port}`);
  console.log(`Cache: ${config.cache.provider} (TTL: ${config.cache.ttlSeconds}s)`);
  console.log(`Features: ${Object.keys(config.features).join(", ")}`);
}

printConfig(config);

Expected output:

App: DodaTech Scanner v2.1.0
Database: localhost:5432
Cache: redis (TTL: 3600s)
Features: realTimeScanning, autoUpdate, betaFeatures

What's Next

With interfaces under your belt, explore type aliases for more flexible type definitions:

Lesson Description
{{< ref "/programming-languages/typescript/03-basic-types" >}} Review basic types
{{< ref "/programming-languages/typescript/05-type-aliases" >}} Union, intersection, and literal types
{{< ref "/programming-languages/typescript/06-functions" >}} Typed function parameters and return values

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro