|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/interfaces
$cat docs/typescript-—-interfaces.md
updated Recently·14 min read·published

TypeScript — Interfaces

TypeScriptIntermediate
Introduction

Interfaces describe the shape of objects — their properties, methods, and structure. They are the primary way to define contracts in TypeScript and are central to object-oriented patterns. Interfaces are extendable, mergeable, and tree-shakeable.

Basic Interface Syntax
basic.ts
TypeScript
1// Basic interface
2interface User {
3 name: string;
4 age: number;
5 email: string;
6}
7
8// Interface with methods
9interface Greeter {
10 greet(message: string): string;
11 farewell(): string;
12}
13
14// Interface with both properties and methods
15interface Repository<T> {
16 findAll(): T[];
17 findById(id: string): T | undefined;
18 save(item: T): void;
19 delete(id: string): boolean;
20}
21
22// Implementing an interface
23class UserRepository implements Repository<User> {
24 private users: User[] = [];
25
26 findAll(): User[] {
27 return this.users;
28 }
29
30 findById(id: string): User | undefined {
31 return this.users.find((u) => u.name === id);
32 }
33
34 save(user: User): void {
35 this.users.push(user);
36 }
37
38 delete(name: string): boolean {
39 const idx = this.users.findIndex((u) => u.name === name);
40 if (idx !== -1) {
41 this.users.splice(idx, 1);
42 return true;
43 }
44 return false;
45 }
46}
Optional and Readonly Properties
optional_readonly.ts
TypeScript
1// Optional properties — suffix with ?
2interface Config {
3 host: string;
4 port: number;
5 debug?: boolean; // optional
6 ssl?: { cert: string; key: string }; // optional nested
7}
8
9const config: Config = { host: "localhost", port: 3000 }; // OK
10const debugConfig: Config = {
11 host: "localhost",
12 port: 3000,
13 debug: true,
14};
15
16// Readonly properties — cannot be reassigned after creation
17interface Coordinates {
18 readonly x: number;
19 readonly y: number;
20}
21
22const point: Coordinates = { x: 1, y: 2 };
23// point.x = 5; // Error: Cannot assign to 'x' because it is a read-only property
24
25// ReadonlyArray — readonly tuple-like interface
26interface Path {
27 readonly points: ReadonlyArray<Coordinates>;
28}
29
30// Readonly vs mutable
31function processPoint(p: Coordinates) {
32 console.log(p.x, p.y);
33 // p.x = 0; // Error
34}
35
36// Deep readonly — use utility types for full immutability
37interface AppState {
38 user: {
39 name: string;
40 preferences: {
41 theme: "light" | "dark";
42 language: string;
43 };
44 };
45}
46type DeepReadonly<T> = {
47 readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
48};

info

Use readonly for interface properties that should not change after creation. For arrays, use ReadonlyArray<T> or the readonly tuple syntax.
Index Signatures

Index signatures allow interfaces to describe properties that are not known at definition time — like dictionaries or maps with dynamic keys.

index_signatures.ts
TypeScript
1// String index signature
2interface StringMap {
3 [key: string]: string;
4}
5
6const headers: StringMap = {
7 "Content-Type": "application/json",
8 Authorization: "Bearer token123",
9};
10
11// Numeric index signature
12interface NumberGrid {
13 [row: number]: number[];
14}
15
16const grid: NumberGrid = {
17 0: [1, 2, 3],
18 1: [4, 5, 6],
19};
20
21// Index signature with named properties
22interface Dictionary {
23 [key: string]: string;
24 length: number; // must be compatible with string index
25}
26
27// Record<string, T> is the idiomatic shorthand
28const scores: Record<string, number> = {
29 alice: 95,
30 bob: 87,
31};
32
33// Readonly index signature
34interface FrozenMap {
35 readonly [key: string]: number;
36}
37
38// Map with both string and number index
39interface Mixed {
40 [key: string]: string | number;
41 [key: number]: boolean;
42}
43
44// Using index signatures for configuration
45interface Env {
46 [key: `VITE_${string}`]: string;
47}
48
49declare const env: Env;
50const apiUrl = env.VITE_API_URL; // string
51// env.DATABASE_URL; // Error: not in pattern

warning

Index signatures can weaken type safety. If you know the exact keys at definition time, prefer named properties instead of index signatures.
Call and Construct Signatures
call_construct.ts
TypeScript
1// Call signature — interface describes a callable function
2interface SearchFunc {
3 (source: string, query: string): boolean;
4}
5
6const search: SearchFunc = (source, query) => {
7 return source.includes(query);
8};
9search("hello world", "hello"); // true
10
11// Call signature with properties
12interface Logger {
13 (message: string): void;
14 level: string;
15 setLevel(level: string): void;
16}
17
18function createLogger(level: string): Logger {
19 const log = ((msg: string) => console.log(`[${level}] ${msg}`)) as Logger;
20 log.level = level;
21 log.setLevel = (newLevel: string) => {
22 log.level = newLevel;
23 };
24 return log;
25}
26
27// Construct signature — describes a class constructor
28interface ClockConstructor {
29 new (hour: number, minute: number): ClockInterface;
30}
31
32interface ClockInterface {
33 tick(): void;
34}
35
36function createClock(
37 ctor: ClockConstructor,
38 hour: number,
39 minute: number
40): ClockInterface {
41 return new ctor(hour, minute);
42}
43
44class DigitalClock implements ClockInterface {
45 constructor(private h: number, private m: number) {}
46 tick(): void {
47 console.log(`beep beep ${this.h}:${this.m}`);
48 }
49}
50
51class AnalogClock implements ClockInterface {
52 constructor(private h: number, private m: number) {}
53 tick(): void {
54 console.log(`tick tock ${this.h}:${this.m}`);
55 }
56}
57
58const digital = createClock(DigitalClock, 12, 0);
59const analog = createClock(AnalogClock, 12, 0);
60digital.tick(); // beep beep 12:0
61analog.tick(); // tick tock 12:0
Extending Interfaces
extends.ts
TypeScript
1// Basic extension
2interface Animal {
3 name: string;
4 weight: number;
5}
6
7interface Dog extends Animal {
8 breed: string;
9 bark(): void;
10}
11
12const rex: Dog = {
13 name: "Rex",
14 weight: 30,
15 breed: "German Shepherd",
16 bark: () => console.log("Woof!"),
17};
18
19// Multiple extension (like mixins)
20interface Printable {
21 print(): string;
22}
23
24interface Loggable {
25 log(message: string): void;
26}
27
28interface Auditable extends Printable, Loggable {
29 auditId: number;
30 auditDate: Date;
31}
32
33// Extending with overrides (narrowing allowed)
34interface Base {
35 value: string | number;
36}
37
38interface Narrowed extends Base {
39 value: string; // narrows the type — OK
40}
41
42// Chain of extensions
43interface HasId {
44 id: string;
45}
46
47interface HasTimestamps {
48 createdAt: Date;
49 updatedAt: Date;
50}
51
52interface HasMetadata {
53 tags: string[];
54}
55
56interface BaseEntity extends HasId, HasTimestamps, HasMetadata {
57 name: string;
58}
59
60interface User extends BaseEntity {
61 email: string;
62 role: "admin" | "user" | "viewer";
63}
64
65// User now has: id, createdAt, updatedAt, tags, name, email, role
Interface Merging (Declaration Merging)

When you declare the same interface multiple times, TypeScript merges them automatically. This is the foundation of module augmentation and declaration merging.

merging.ts
TypeScript
1// Simple merging — properties from both declarations combine
2interface Box {
3 width: number;
4}
5
6interface Box {
7 height: number;
8}
9
10// Box is now { width: number; height: number }
11const box: Box = { width: 10, height: 20 };
12
13// Merging with methods
14interface Counter {
15 count: number;
16}
17
18interface Counter {
19 increment(): void;
20 decrement(): void;
21 reset(): void;
22}
23
24// Counter now has count + increment + decrement + reset
25
26// Module augmentation — extend existing modules
27// In typescript declaration files (.d.ts)
28declare module "express" {
29 interface Request {
30 userId?: string;
31 sessionId?: string;
32 }
33}
34
35// After augmentation, req.userId is available in Express handlers
36// app.get("/", (req) => { req.userId; }) — type-safe
37
38// Namespace merging
39interface Cloner {
40 clone(): HTMLElement;
41}
42
43interface Cloner {
44 clone(deep: boolean): HTMLElement;
45}
46
47// Cloner now has two clone signatures (like overloads)
48
49// Merge rules:
50// 1. Properties with the same name must have the same type
51// 2. Methods with the same name merge (become overloads)
52// 3. Non-function members must be unique (or identical)
53// 4. The order of declarations doesn't matter

best practice

Declaration merging is powerful for extending third-party types. Use module augmentation in .d.ts files to add properties to existing libraries like Express or React.
Interfaces vs Type Aliases
vs_type_aliases.ts
TypeScript
1// Interface — open for extension (declaration merging)
2interface User {
3 name: string;
4}
5interface User {
6 age: number;
7}
8// User = { name: string; age: number } — merged
9
10// Type alias — closed, cannot re-declare
11type UserAlias = { name: string };
12// type UserAlias = { age: number }; // Error: Duplicate identifier
13
14// Interface extends — class-like syntax
15interface Animal {
16 name: string;
17}
18interface Dog extends Animal {
19 breed: string;
20}
21
22// Type alias intersection — more flexible composition
23type AnimalAlias = { name: string };
24type DogAlias = AnimalAlias & { breed: string };
25
26// When to use which:
27// Interface: object shapes, class contracts, public APIs
28// Type alias: unions, intersections, mapped types, primitives, tuples
29
30// Interface can be used with extends (class-like)
31interface Point {
32 x: number;
33 y: number;
34}
35interface Point3D extends Point {
36 z: number;
37}
38
39// Type alias can be a union (interfaces cannot)
40type Result = { ok: true; data: string } | { ok: false; error: string };
41
42// Type alias for primitives
43type ID = string | number;
44type UserID = string & { __brand: "UserID" }; // branded type
45
46// Both work for function types
47type Formatter = (input: string) => string;
48interface FormatterInterface {
49 (input: string): string;
50}
51
52// Both work for arrays
53type StringArray = string[];
54interface StringArrayInterface extends Array<string> {}
55
56// Performance: interfaces may be faster for type checking in large codebases
57// because they can be compared by reference (declaration merging creates
58// a single cached type). Type aliases may create new types each time.

info

Use interfaces for object shapes and public APIs where extensibility matters. Use type aliases for unions, intersections, and more complex type compositions.
Best Practices

1. Use interfaces for public API contracts — they are extensible via declaration merging and provide better error messages.

2. Prefer ReadonlyArray<T> and readonly modifiers for data that should not be mutated.

3. Avoid index signatures when you know the exact keys — named properties provide better autocompletion and type safety.

4. Use generic interfaces like Repository<T> or ApiResponse<T> for reusable type-safe patterns.

5. Use declaration merging to augment third-party types in .d.ts files — never modify the library source directly.

6. Prefer extension (extends) over intersection (&) for combining object types — it produces cleaner error messages.

7. Name interfaces after the contract they describe: UserService, CacheProvider, EventEmitter.

8. Avoid any in interface properties — use unknown if you need flexibility and narrow at usage sites.

$Blueprint — Engineering Documentation·Section ID: TS-IFACE·Revision: 1.0