Question

TypeScript Function Overloading Explained

typescriptoverloading

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.

Take Quiz

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:

  1. TypeScript sees the call formatter.format(42.678).

  2. It checks the overload signatures:

    • format(value: string): string
    • format(value: number): string
  3. The argument is a number, so TypeScript chooses:

    format(value: number): string;
    
  4. At runtime, JavaScript calls the single implementation:

    format(value: string | number): string { ... }
    
  5. Inside the method, typeof value === 'number' is true.

  6. The method returns value.toFixed(2).

  7. result becomes:

    "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.

Take Quiz

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

ApproachBest forExampleNotes
Function overloadsDifferent valid call signaturesfn(id: number) and fn(options: object)Best when callers benefit from distinct signatures
Union typesOne implementation with flexible input`fn(value: stringnumber)`
Optional parametersMissing arguments are acceptablefn(name: string, age?: number)Good when later arguments are truly optional
Rest parametersVariable number of argumentsfn(...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
Take Quiz

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.

Take Quiz

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 ProductStore class with a list of sample products.
  • Add overloaded findProduct method signatures for number and string inputs.
  • Implement a single findProduct method body that handles both cases.
  • Return the matching product or undefined if nothing is found.
  • Test the method with both an ID and a name.
Take Quiz

Keep learning

Related questions

View more