Interface Design and Type Contracts

1. Interface Declaration and Extension

Feature Syntax Description Use Case
Basic Interface interface Name { } Define object shape with properties and methods Object contracts, API shapes
Interface Extension interface B extends A Inherit properties from parent interface Type hierarchies, specialization
Multiple Extension extends A, B, C Extend multiple interfaces simultaneously Mixin patterns, composition
Hybrid Type Callable + properties Object that is both callable and has properties Function objects, jQuery-like APIs

Example: Basic interface declaration

// Basic interface
interface User {
    id: number;
    name: string;
    email: string;
}

const user: User = {
    id: 1,
    name: "Alice",
    email: "alice@email.com"
};

// Interface with methods
interface Calculator {
    add(a: number, b: number): number;
    subtract(a: number, b: number): number;
}

const calc: Calculator = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
};

// Nested interfaces
interface Address {
    street: string;
    city: string;
    country: string;
}

interface Person {
    name: string;
    address: Address;
}

Example: Interface extension

// Single extension
interface Animal {
    name: string;
    age: number;
}

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

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

// Multiple extension
interface Timestamped {
    createdAt: Date;
    updatedAt: Date;
}

interface Versioned {
    version: number;
}

interface Document extends Timestamped, Versioned {
    id: string;
    content: string;
}

// Override inherited properties
interface Shape {
    color: string;
}

interface ColoredSquare extends Shape {
    color: "red" | "blue" | "green";  // Narrowed type
    size: number;
}

// Extension chain
interface Entity {
    id: string;
}

interface NamedEntity extends Entity {
    name: string;
}

interface User extends NamedEntity {
    email: string;
}

2. Optional Properties and Readonly Modifiers

Modifier Syntax Description Use Case
Optional Property prop?: Type Property may be undefined or missing Partial data, optional fields
Readonly Property readonly prop: Type Property cannot be reassigned after initialization Immutable data, constants
Readonly Optional readonly prop?: Type Combine both modifiers - optional and immutable Optional config values

Example: Optional properties

// Optional properties
interface UserProfile {
    id: number;
    name: string;
    email: string;
    phone?: string;          // Optional
    avatar?: string;         // Optional
    bio?: string;            // Optional
}

// Valid with or without optional properties
const user1: UserProfile = {
    id: 1,
    name: "Alice",
    email: "alice@email.com"
};

const user2: UserProfile = {
    id: 2,
    name: "Bob",
    email: "bob@email.com",
    phone: "555-1234",
    avatar: "avatar.jpg"
};

// Function accepting optional properties
function createProfile(data: UserProfile): void {
    console.log(data.name);
    // Check optional properties before use
    if (data.phone) {
        console.log(`Phone: ${data.phone}`);
    }
}

Example: Readonly properties

// Readonly properties
interface Config {
    readonly apiUrl: string;
    readonly apiKey: string;
    readonly timeout: number;
    retries: number;  // Mutable
}

const config: Config = {
    apiUrl: "https://api.example.com",
    apiKey: "secret-key",
    timeout: 5000,
    retries: 3
};

// config.apiUrl = "https://new-url.com";  // Error: readonly
// config.timeout = 10000;                  // Error: readonly
config.retries = 5;  // OK - mutable

// Readonly arrays and objects
interface Data {
    readonly tags: readonly string[];
    readonly metadata: Readonly<{ key: string; value: string }>;
}

const data: Data = {
    tags: ["typescript", "tutorial"],
    metadata: { key: "author", value: "Alice" }
};

// data.tags.push("new");              // Error: readonly array
// data.metadata.key = "new";          // Error: readonly object

// Readonly with optional
interface Settings {
    readonly id: string;
    readonly theme?: "light" | "dark";
    readonly fontSize?: number;
}

3. Function Types and Method Signatures in Interfaces

Pattern Syntax Description Example
Method Signature method(args): Type Method within interface - regular syntax getName(): string
Function Property prop: (args) => Type Property that is a function - arrow syntax callback: () => void
Call Signature (args): Type Make interface itself callable (x: number): string
Construct Signature new (args): Type Constructor function signature new (name: string): User
Overloaded Methods Multiple signatures Method with multiple call signatures Different parameter combinations

Example: Method signatures

// Method signatures
interface Repository<T> {
    find(id: string): T | undefined;
    findAll(): T[];
    save(item: T): void;
    delete(id: string): boolean;
    count(): number;
}

// Function properties vs method signatures
interface EventEmitter {
    // Method signature (preferred)
    on(event: string, handler: Function): void;
    off(event: string, handler: Function): void;
    
    // Function property (allows reassignment)
    emit: (event: string, ...args: any[]) => void;
}

// Optional methods
interface Logger {
    log(message: string): void;
    error(message: string): void;
    warn?(message: string): void;  // Optional method
    debug?(message: string): void; // Optional method
}

// Generic method signatures
interface Transformer {
    transform<T, U>(input: T): U;
    validate<T>(data: T): boolean;
}

Example: Call and construct signatures

// Call signature - callable interface
interface StringFormatter {
    (input: string): string;  // Call signature
    prefix: string;            // Also has properties
    suffix: string;
}

function createFormatter(pre: string, suf: string): StringFormatter {
    const formatter = ((input: string) => {
        return `${formatter.prefix}${input}${formatter.suffix}`;
    }) as StringFormatter;
    
    formatter.prefix = pre;
    formatter.suffix = suf;
    return formatter;
}

const format = createFormatter("[", "]");
console.log(format("test"));  // "[test]"

// Construct signature - constructor interface
interface UserConstructor {
    new (name: string, age: number): User;
}

class User {
    constructor(public name: string, public age: number) {}
}

function createUser(ctor: UserConstructor, name: string, age: number): User {
    return new ctor(name, age);
}

const user = createUser(User, "Alice", 30);

// Hybrid type - callable with properties
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

Example: Method overloading in interfaces

// Overloaded method signatures
interface Formatter {
    format(value: string): string;
    format(value: number): string;
    format(value: boolean): string;
    format(value: Date): string;
}

const formatter: Formatter = {
    format(value: string | number | boolean | Date): string {
        if (typeof value === "string") return value;
        if (typeof value === "number") return value.toString();
        if (typeof value === "boolean") return value ? "true" : "false";
        return value.toISOString();
    }
};

// Generic overloads
interface Parser {
    parse(input: string): any;
    parse<T>(input: string, type: new () => T): T;
}

// Rest parameters in interface
interface Logger {
    log(message: string, ...args: any[]): void;
}

4. Index Signatures for Dynamic Properties

Type Syntax Description Use Case
String Index [key: string]: Type Any string key maps to specified type Dictionaries, dynamic objects
Number Index [index: number]: Type Numeric indices for array-like structures Array-like objects, tuples
Symbol Index TS 4.4+ [key: symbol]: Type Symbol keys for unique properties Well-known symbols, metadata
Template Literal Index [K: `prefix${string}`] Pattern-based property keys Prefixed properties, conventions

Example: String index signatures

// Basic string index signature
interface Dictionary<T> {
    [key: string]: T;
}

const scores: Dictionary<number> = {
    "Alice": 95,
    "Bob": 87,
    "Charlie": 92
};

// Mixed: known properties + index signature
interface UserData {
    id: number;
    name: string;
    [key: string]: any;  // Additional dynamic properties
}

const userData: UserData = {
    id: 1,
    name: "Alice",
    age: 30,              // OK - additional property
    email: "alice@email.com"  // OK
};

// Readonly index signature
interface ReadonlyDict {
    readonly [key: string]: string;
}

const dict: ReadonlyDict = { a: "hello" };
// dict.a = "world";  // Error: readonly
// dict.b = "new";    // Error: readonly

// Constrained index signature
interface NumberDict {
    [key: string]: number;
    length: number;  // OK - number type
    // name: string; // Error: must be number
}

Example: Number and advanced index signatures

// Number index signature
interface StringArray {
    [index: number]: string;
}

const arr: StringArray = ["a", "b", "c"];
const first: string = arr[0];

// Both string and number index
interface MixedArray {
    [index: number]: string;
    [key: string]: string | number;
    length: number;
}

// Template literal index signature (TS 4.4+)
interface DataAttributes {
    [key: `data-${string}`]: string;
}

const attrs: DataAttributes = {
    "data-id": "123",
    "data-name": "test",
    // "id": "456"  // Error: must match pattern
};

// Generic index signature
interface Cache<T> {
    [key: string]: T | undefined;
    get(key: string): T | undefined;
    set(key: string, value: T): void;
}

// Symbol index signature (TS 4.4+)
interface Symbolized {
    [key: symbol]: any;
}

const sym = Symbol("id");
const obj: Symbolized = {
    [sym]: 123
};

5. Interface Merging and Declaration Merging

Concept Description Use Case
Declaration Merging Multiple interface declarations with same name merge into one Augmenting libraries, modules
Module Augmentation Add types to existing modules or libraries Extending third-party types
Global Augmentation Add properties to global namespace Window, process extensions
Namespace Merging Interfaces can merge with namespaces/functions Hybrid declarations

Example: Declaration merging

// Basic interface merging
interface User {
    id: number;
    name: string;
}

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

// Merged result:
// interface User {
//     id: number;
//     name: string;
//     email: string;
//     age: number;
// }

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

// Method overloading via merging
interface Calculator {
    add(a: number, b: number): number;
}

interface Calculator {
    add(a: string, b: string): string;
}

// Both overloads available
const calc: Calculator = {
    add(a: any, b: any): any {
        return a + b;
    }
};

Example: Module and global augmentation

// Augment existing module
declare module "express" {
    interface Request {
        user?: {
            id: string;
            name: string;
        };
    }
}

// Global augmentation
declare global {
    interface Window {
        customProperty: string;
        myAPI: {
            version: string;
            init(): void;
        };
    }
    
    interface Array<T> {
        customMethod(): T[];
    }
}

// Now available globally
window.customProperty = "value";
window.myAPI.init();

// Augment NodeJS global
declare global {
    namespace NodeJS {
        interface ProcessEnv {
            CUSTOM_VAR: string;
            API_KEY: string;
        }
    }
}

// Type-safe environment variables
const apiKey = process.env.API_KEY;  // string
Note: Interface merging only works with interfaces, not type aliases. This is one key difference between interfaces and types. Use merging for library augmentation and extending third-party types.

6. Implementing Interfaces in Classes

Feature Syntax Description Use Case
Single Implementation class X implements I Class must satisfy interface contract Type contracts, polymorphism
Multiple Implementation implements I1, I2, I3 Class implements multiple interfaces Multiple capabilities, composition
Interface + Extension extends A implements I Combine inheritance and interface implementation OOP patterns, frameworks
Generic Implementation class X<T> implements I<T> Implement generic interface with type parameter Generic containers, adapters

Example: Basic interface implementation

// Define interface
interface Printable {
    print(): void;
}

interface Serializable {
    serialize(): string;
    deserialize(data: string): void;
}

// Implement single interface
class Document implements Printable {
    constructor(private content: string) {}
    
    print(): void {
        console.log(this.content);
    }
}

// Implement multiple interfaces
class DataModel implements Printable, Serializable {
    constructor(private data: any) {}
    
    print(): void {
        console.log(JSON.stringify(this.data, null, 2));
    }
    
    serialize(): string {
        return JSON.stringify(this.data);
    }
    
    deserialize(data: string): void {
        this.data = JSON.parse(data);
    }
}

// Interface ensures implementation
// class InvalidDoc implements Printable {
//     // Error: missing print() method
// }

Example: Generic interface implementation

// Generic interface
interface Repository<T> {
    find(id: string): T | undefined;
    findAll(): T[];
    save(item: T): void;
    delete(id: string): boolean;
}

// Implement with specific type
class UserRepository implements Repository<User> {
    private users: Map<string, User> = new Map();
    
    find(id: string): User | undefined {
        return this.users.get(id);
    }
    
    findAll(): User[] {
        return Array.from(this.users.values());
    }
    
    save(user: User): void {
        this.users.set(user.id.toString(), user);
    }
    
    delete(id: string): boolean {
        return this.users.delete(id);
    }
}

// Generic class implementing generic interface
class GenericRepository<T extends { id: string }> implements Repository<T> {
    private items = new Map<string, T>();
    
    find(id: string): T | undefined {
        return this.items.get(id);
    }
    
    findAll(): T[] {
        return Array.from(this.items.values());
    }
    
    save(item: T): void {
        this.items.set(item.id, item);
    }
    
    delete(id: string): boolean {
        return this.items.delete(id);
    }
}

Example: Combining inheritance and interfaces

// Base class
class Entity {
    constructor(public id: string, public createdAt: Date) {}
}

// Interfaces
interface Timestamped {
    updatedAt: Date;
    touch(): void;
}

interface Versioned {
    version: number;
    incrementVersion(): void;
}

// Extend class and implement interfaces
class Document extends Entity implements Timestamped, Versioned {
    updatedAt: Date;
    version: number;
    
    constructor(
        id: string,
        public content: string
    ) {
        super(id, new Date());
        this.updatedAt = new Date();
        this.version = 1;
    }
    
    touch(): void {
        this.updatedAt = new Date();
    }
    
    incrementVersion(): void {
        this.version++;
        this.touch();
    }
}

// Polymorphism with interfaces
function printTimestamp(obj: Timestamped): void {
    console.log(`Updated: ${obj.updatedAt}`);
}

const doc = new Document("1", "Hello");
printTimestamp(doc);  // OK - Document implements Timestamped

Interface Design Best Practices

  • Use interfaces for object shapes and public contracts
  • Prefer readonly for immutable properties
  • Use optional properties for partial data
  • Apply index signatures for flexible dictionaries
  • Leverage declaration merging for library augmentation
  • Implement interfaces in classes for polymorphism
  • Use method signatures over function properties for better type checking