TypeScript Interfaces
1. Interface Declaration and Extension (extends keyword)
| 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 Modifier
| 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 ([key: string]: type) 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 (implements keyword)
| 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