TypeScript Interfaces — Complete Guide with Examples
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
Can an interface extend multiple interfaces? Yes:
interface C extends A, B { ... }What is the difference between
readonlyin an interface andconst?readonlyapplies to a property (cannot reassign), whileconstapplies to a variable.readonlydoes not prevent mutation of arrays/objects, just reassignment.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.
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
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