Question
I am reading about function overloading in the TypeScript language specification, and the examples suggest that a function can have multiple call signatures. However, when I try to write overloads like this, the compiler reports a duplicate identifier error, even though the parameter types are different.
export class LayerFactory {
constructor(public styleFactory: Symbology.StyleFactory) {
// Init...
}
createFeatureLayer(userContext: Model.UserContext, mapWrapperObj: MapWrapperBase): any {
throw new Error('not implemented');
}
createFeatureLayer(layerName: string, style: any): any {
throw new Error('not implemented');
}
}
I also still get the same error if I change the second method to have a different number of parameters. How is function overloading supposed to work in TypeScript, and how should this code be written correctly?
Short Answer
By the end of this page, you will understand how TypeScript function overloading actually works, why writing multiple method bodies causes a duplicate identifier error, and how to define overload signatures with a single implementation. You will also see practical examples, common mistakes, and patterns used in real TypeScript codebases.
Concept
TypeScript supports function overloading, but not in the same way some other languages do.
In TypeScript, overloading means:
- you declare multiple function signatures for callers
- but you provide exactly one implementation
That means this is allowed in principle:
function example(x: string): string;
function example(x: number): number;
function example(x: string | number): string | number {
return x;
}
The first two lines are overload signatures. They describe valid ways to call the function.
The last line is the implementation signature. This is the only place where the actual code exists.
Your original code defines two separate method implementations with the same name inside the class. JavaScript, which TypeScript compiles to, does not support multiple method bodies with the same name. So TypeScript reports a duplicate identifier error.
Why this matters:
- It gives better type safety for callers.
- It lets one function support different input shapes.
- It keeps runtime code simple, because JavaScript still has only one real function.
So the key rule is:
- Many overload signatures, one implementation
When using overloads, the implementation must be broad enough to handle all declared overload cases.
Mental Model
Think of function overloading like a customer service desk with multiple signs:
- Sign 1: "Questions about billing"
- Sign 2: "Questions about shipping"
- Sign 3: "Questions about returns"
To the customer, it looks like there are different entry points.
But behind the desk, there is still one employee handling all requests.
In TypeScript:
- the overload signatures are the signs shown to users of the function
- the implementation is the one employee doing the actual work
If you try to create multiple employees with the exact same desk name in the same place, TypeScript complains. That is what happened in your class: you wrote multiple method bodies instead of multiple signatures plus one shared implementation.
Syntax and Examples
The correct syntax for overloading in TypeScript is:
function fn(a: string): string;
function fn(a: number): number;
function fn(a: string | number): string | number {
return a;
}
Class method overloads
For a class method, write the overload signatures first, then a single implementation:
export class LayerFactory {
constructor(public styleFactory: Symbology.StyleFactory) {
// Init...
}
createFeatureLayer(userContext: Model.UserContext, mapWrapperObj: MapWrapperBase): any;
createFeatureLayer(layerName: string, style: any): any;
createFeatureLayer(
arg1: Model.UserContext | string,
arg2: MapWrapperBase | any
): any {
if (typeof arg1 === 'string') {
const layerName = arg1;
const style = arg2;
throw new Error(`not implemented: create by name ${layerName}`);
} else {
const userContext = arg1;
const mapWrapperObj = arg2 as MapWrapperBase;
throw new Error('not implemented: create by context');
}
}
}
What this means
- The first two
createFeatureLayer(...)lines end with;and have no body. - These are overload signatures.
- The final
createFeatureLayer(...) { ... }is the only implementation.
Beginner-friendly example
class Formatter {
format(value: string): string;
format(value: number): string;
format(value: string | number): string {
if (typeof value === 'number') {
return value.toFixed(2);
}
return value.trim();
}
}
const formatter = new Formatter();
formatter.format(12.345); // "12.35"
formatter.format(" hello "); // "hello"
The caller gets precise types, but the class only contains one real format method.
Step by Step Execution
Consider this example:
class Formatter {
format(value: string): string;
format(value: number): string;
format(value: string | number): string {
if (typeof value === 'number') {
return value.toFixed(2);
}
return value.trim();
}
}
const formatter = new Formatter();
const result = formatter.format(42.678);
Step by step:
-
TypeScript sees the call
formatter.format(42.678). -
It checks the overload signatures:
format(value: string): stringformat(value: number): string
-
The argument is a number, so TypeScript chooses:
format(value: number): string; -
At runtime, JavaScript calls the single implementation:
format(value: string | number): string { ... } -
Inside the method,
typeof value === 'number'istrue. -
The method returns
value.toFixed(2). -
resultbecomes:"42.68"
Important detail:
- The overload signatures are used by the type checker.
- The implementation body is used at runtime.
Real World Use Cases
Function overloads are useful when one function supports a few clearly different calling patterns.
Common real uses
- Parsing input
- Accept a string, number, or options object.
- API wrappers
- Accept either an ID or a full configuration object.
- UI helpers
- Create a component from different parameter combinations.
- Data formatting
- Format numbers, dates, or strings using one method name.
- Library design
- Expose a friendly API while keeping one implementation internally.
Example: fetch by ID or options
function getUser(id: number): string;
function getUser(options: { email: string }): string;
function getUser(arg: number | { email: string }): string {
if (typeof arg === 'number') {
return `Fetching user by id: ${arg}`;
}
return `Fetching user by email: ${arg.email}`;
}
This lets callers use the same function name in multiple valid ways while keeping type checking clear.
Real Codebase Usage
In real projects, developers often combine overloads with runtime checks and simple branching.
Common patterns
Guard clauses
Use early checks to separate cases clearly.
function parse(input: string): string[];
function parse(input: string[]): string[];
function parse(input: string | string[]): string[] {
if (Array.isArray(input)) {
return input;
}
return input.split(',');
}
Validation before processing
Overloads describe valid inputs, but runtime validation is still useful.
function openFile(path: string): void;
function openFile(config: { path: string }): void;
function openFile(arg: string | { path: string }): void {
const path = typeof arg === 'string' ? arg : arg.path;
if (!path) {
throw new Error('Path is required');
}
console.log(`Opening ${path}`);
}
Library-style APIs
Many libraries use overloads when a function can be called in a few distinct, meaningful ways.
Prefer unions when overloads add little value
If behavior is basically the same, a union can be simpler than overloads.
function print(value: string | number): void {
console.log(value);
}
In real codebases, good overloads are:
- small in number
- easy to understand
- paired with clear runtime checks
- used when they improve the API for callers
Common Mistakes
1. Writing multiple implementations
This is the mistake from the original question.
Broken code
class Example {
doThing(value: string): void {
console.log(value);
}
doThing(value: number): void {
console.log(value);
}
}
This fails because there are two method bodies.
Correct version
class Example {
doThing(value: string): void;
doThing(value: number): void;
doThing(value: string | number): void {
console.log(value);
}
}
2. Making the implementation too narrow
Broken code
function greet(name: string): void;
function greet(age: number): void;
function greet(name: string): void {
console.log(name);
}
The implementation only accepts string, so it cannot handle the number overload.
Correct version
function greet(name: string): void;
function greet(age: number): void;
function greet(value: string | number): void {
console.log(value);
}
3. Forgetting runtime checks
TypeScript types disappear at runtime. You still need JavaScript logic to tell cases apart.
function double(value: string): string;
function double(value: number): number;
function double(value: string | number): string | number {
if (typeof value === 'number') {
return value * 2;
}
return value + value;
}
4. Using overloads when a union is simpler
If the return type and behavior do not change much, overloads may be unnecessary.
function log(value: string | number): void {
console.log(value);
}
5. Expecting JavaScript-style runtime overloading
JavaScript does not support true runtime overloading by parameter type. TypeScript only adds static type checking on top of JavaScript.
Comparisons
| Approach | Best for | Example | Notes |
|---|---|---|---|
| Function overloads | Different valid call signatures | fn(id: number) and fn(options: object) | Best when callers benefit from distinct signatures |
| Union types | One implementation with flexible input | `fn(value: string | number)` |
| Optional parameters | Missing arguments are acceptable | fn(name: string, age?: number) | Good when later arguments are truly optional |
| Rest parameters | Variable number of arguments | fn(...items: string[]) | Good for lists of values |
Overloads vs union types
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
return String(value);
}
vs
function format(value: string | number): string {
return String(value);
}
Use overloads when:
- different call forms should be documented clearly
- return types differ by input
- API usability improves
Use unions when:
- behavior is mostly the same
- the function is simple
- overloads would add noise
Cheat Sheet
// Overload signatures
function fn(a: string): string;
function fn(a: number): number;
// Single implementation
function fn(a: string | number): string | number {
return a;
}
Rules
- Write one or more overload signatures.
- End overload signatures with
;. - Provide exactly one implementation.
- The implementation must handle all overload cases.
- Use runtime checks like
typeof,Array.isArray, or property checks.
Class method pattern
class Example {
method(x: string): string;
method(x: number): number;
method(x: string | number): string | number {
return x;
}
}
Common runtime checks
typeof value === 'string'
typeof value === 'number'
Array.isArray(value)
'propertyName' in obj
When to use overloads
- same function name
- a few distinct valid call patterns
- caller-facing types should be precise
When not to use overloads
- one union type is enough
- behavior is identical for all inputs
- too many overloads would make the API confusing
FAQ
Why do I get a duplicate identifier error in TypeScript overloads?
Because you wrote more than one function or method body with the same name. TypeScript overloads require multiple signatures but only one implementation.
How do I overload a method in a TypeScript class?
Write the method signatures first without bodies, then add one final method with the actual implementation.
Does TypeScript support runtime function overloading?
No. At runtime, JavaScript only has one real function. TypeScript overloads are for static type checking.
Should I use overloads or union types?
Use overloads when different call patterns need clearer typing. Use unions when one flexible parameter type is enough.
Can overloaded functions return different types?
Yes. Different overload signatures can specify different return types, as long as the implementation can handle all cases.
Why must the implementation be more general?
Because it has to accept every valid overload case. If one overload accepts a number, the implementation must also be able to receive a number.
Can I overload arrow functions?
Not directly in the same way as function declarations, but you can type a variable with multiple call signatures and assign one implementation to it.
Mini Project
Description
Build a small TypeScript utility class that can search for a product in two different ways: by numeric ID or by product name. This demonstrates how overloads provide a clean API while still using only one implementation method internally.
Goal
Create a class method with overloads that accepts either a product ID or a product name and returns the matching product.
Requirements
- Create a
ProductStoreclass with a list of sample products. - Add overloaded
findProductmethod signatures fornumberandstringinputs. - Implement a single
findProductmethod body that handles both cases. - Return the matching product or
undefinedif nothing is found. - Test the method with both an ID and a name.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.