TypeScript Declaration Merging and Function Overloading: Interfaces, Namespaces & Overloads
1. What is declaration merging in TypeScript?
Declaration Merging: TypeScript's ability to combine multiple declarations of the same name into a single definition.
- Applies to: Interfaces, namespaces, and modules.
- Purpose: Allows extending existing types without modifying their original definitions.
- Use Case: Adding properties/methods to existing types (e.g., extending library types or global objects).
- Key Points:
- Declarations with the same name are merged into a single type.
- Commonly used with interfaces and namespaces.
Can you give an example of declaration merging?
// Declaration merging example
interface User { name: string;
} // Merge additional properties into User
interface User { age: number;
} const user: User = { name: "kristal", age: 25
}; console.log(user); Output:
{ "name": "kristal", "age": 25 } 2. What is interface merging in TypeScript?
Interface Merging: Combining multiple interface declarations with the same name into a single interface.
- Rules:
- Properties with the same name must have compatible types.
- Methods can be merged, but signatures must align.
- Use Case: Extending interfaces in different files or modules, often for augmenting third-party libraries.
- Key Points:
- Non-conflicting members are combined.
- Conflicting members (e.g., different types for the same property) cause a compilation error.
Can you give an example of interface merging?
// Interface merging example
interface Product { id: number; name: string;
} // Merge additional properties and methods
interface Product { price: number; getDetails(): string;
} class Item implements Product { id: number; name: string; price: number; constructor(id: number, name: string, price: number) { this.id = id; this.name = name; this.price = price; } getDetails(): string { return `${this.name} (ID: ${this.id}) costs $${this.price}`; }
} const item = new Item(1, "Laptop", 999.99);
console.log(item.getDetails()); Output:
Laptop (ID: 1) costs $999.99 3. What is namespace merging in TypeScript?
Namespace Merging: Combining multiple namespace declarations with the same name into a single namespace.
- Syntax: Use
namespaceormodulekeyword (thoughnamespaceis preferred). - Rules:
- Namespaces merge their members (variables, functions, interfaces, etc.).
- Members must not conflict (e.g., same name with incompatible types).
- Use Case: Organizing code into logical groups or extending global namespaces (e.g., augmenting
Window). - Key Points:
- Namespaces can merge with other namespaces or interfaces.
- Often used in legacy TypeScript code or for global augmentation.
Can you give an example of namespace merging?
// Namespace merging example
namespace Config { export const apiUrl = "https://api.example.com";
} namespace Config { export function getApiKey(): string { return "abc123"; }
} // Using merged namespace
console.log(`API URL: ${Config.apiUrl}`);
console.log(`API Key: ${Config.getApiKey()}`); Output:
API URL: https://api.example.com
API Key: abc123 4. What is function overloading in TypeScript?
Function Overloading: Defining multiple function signatures for the same function name, allowing different parameter types or counts.
- Syntax:
function funcName(param: Type1): ReturnType1; function funcName(param: Type2): ReturnType2; function funcName(param: any): any { // Implementation } - Rules:
- Overload signatures define possible call patterns.
- Implementation signature must be compatible with all overloads.
- The implementation uses
anyor union types for flexibility.
- Use Case: Supporting multiple input/output types for a single function (e.g., parsing different data formats).
- Key Points:
- TypeScript checks overload signatures at compile time.
- Only the implementation is executed at runtime.
Can you give an example of function overloading?
// Function overloading example
function formatData(data: string): string;
function formatData(data: number): string;
function formatData(data: string | number): string { if (typeof data === "string") { return `String: ${data.toUpperCase()}`; } else { return `Number: ${data.toFixed(2)}`; }
} // Testing overloads
console.log(formatData("hello"));
console.log(formatData(42));
// console.log(formatData(true)); // Error: Argument of type 'boolean' is not assignable Output:
String: HELLO
Number: 42.00 5. Can you provide a comprehensive example combining declaration merging, interface merging, namespace merging, and function overloading?
Project Structure:
ts-declaration-merging/
├── src/
│ └── main.ts
├── tsconfig.json
└── package.json main.ts:
// Comprehensive TypeScript Example
// Interface Merging
interface User { id: number; name: string;
} interface User { email: string; getProfile(): string;
} // Namespace Merging
namespace Settings { export const version = "1.0.0";
} namespace Settings { export interface Config { apiUrl: string; } export const config: Config = { apiUrl: "https://api.example.com" }; export function getVersionInfo(): string { return `Version: ${version}, API: ${config.apiUrl}`; }
} // Function Overloading
function processData(input: string): { type: "string"; value: string };
function processData(input: number): { type: "number"; value: number };
function processData(input: string | number): { type: string; value: string | number } { if (typeof input === "string") { return { type: "string", value: input.toUpperCase() }; } return { type: "number", value: input };
} // Class implementing merged interface
class UserProfile implements User { id: number; name: string; email: string; constructor(id: number, name: string, email: string) { this.id = id; this.name = name; this.email = email; } getProfile(): string { return `ID: ${this.id}, Name: ${this.name}, Email: ${this.email}`; }
} // Example usage
try { // Interface merging const user = new UserProfile(1, "kristal", "[email protected]"); console.log("User Profile:", user.getProfile()); // Namespace merging console.log("Settings:", Settings.getVersionInfo()); console.log("Config:", Settings.config); // Function overloading const stringResult = processData("hello"); const numberResult = processData(42); console.log("String Data:", stringResult); console.log("Number Data:", numberResult); // Error cases // const invalidUser: User = { id: 2, name: "Sashi" }; // Error: Property 'email' is missing // console.log(processData(true)); // Error: Argument of type 'boolean' is not assignable
} catch (e) { console.error(`Error: ${e.message}`);
} package.json:
{ "name": "ts-declaration-merging", "version": "1.0.0", "scripts": { "start": "tsc && node dist/main.js", "build": "tsc", "watch": "tsc --watch" }, "devDependencies": { "typescript": "^5.6.2" }
} tsconfig.json:
{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules"]
} Steps to Run:
- Create the project structure.
- Install dependencies:
npm install. - Compile:
npm run build. - Run:
npm start.
Output:
User Profile: ID: 1, Name: kristal, Email: [email protected]
Settings: Version: 1.0.0, API: https://api.example.com
Config: { "apiUrl": "https://api.example.com" }
String Data: { "type": "string", "value": "HELLO" }
Number Data: { "type": "number", "value": 42 } Description:
- Interface Merging: Combines two
Userinterfaces to includeid,name,email, andgetProfile. - Namespace Merging: Merges
Settingsnamespace to includeversion,config, andgetVersionInfo. - Function Overloading:
processDatahandles bothstringandnumberinputs with distinct return types. - Type Safety: Commented-out code shows TypeScript's compile-time checks preventing invalid assignments.
6. What are common mistakes in these TypeScript features?
- Declaration Merging:
- Declaring conflicting types for the same property, causing compilation errors.
- Overusing merging, leading to complex and hard-to-maintain types.
- Interface Merging:
- Not ensuring compatible types for merged properties/methods.
- Merging interfaces across unrelated modules, causing confusion.
- Namespace Merging:
- Using namespaces in modern TypeScript (ES modules preferred).
- Creating naming conflicts with global namespaces.
- Function Overloading:
- Writing implementation signatures incompatible with overloads.
- Overloading unnecessarily when union types suffice.
- General:
- Not leveraging TypeScript's type inference to simplify code.
- Poor documentation of merged types or overloads.
7. What are best practices for these TypeScript features?
- Declaration Merging:
- Use for extending third-party libraries or global objects (e.g.,
Window). - Keep merged declarations in separate files for clarity.
- Use for extending third-party libraries or global objects (e.g.,
- Interface Merging:
- Ensure merged properties/methods have compatible types.
- Use for modular type extensions in large projects.
- Namespace Merging:
- Prefer ES modules over namespaces in modern TypeScript.
- Use for global augmentation or legacy code support.
- Function Overloading:
- Use overloads only when union types are insufficient.
- Ensure implementation signature is broad enough to cover all overloads.
- General:
- Follow TypeScript naming conventions (e.g., CamelCase for interfaces).
- Use
tscwithstrictmode (strict: trueintsconfig.json). - Document complex types or overloads with JSDoc or comments for clarity.
- Test with edge cases (e.g., invalid types, missing properties).
- Use linters (e.g.,
eslint) with TypeScript plugins for consistency.