Skip to content

Fix: TypeScript Function Overload Error — No Overload Matches This Call

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix TypeScript function overload errors — overload signature compatibility, implementation signature, conditional types as alternatives, method overloads in classes, and common pitfalls.

The Problem

TypeScript rejects a valid call to an overloaded function:

function format(value: string): string;
function format(value: number, decimals: number): string;
function format(value: string | number, decimals?: number): string {
  if (typeof value === 'string') return value.toUpperCase();
  return value.toFixed(decimals ?? 2);
}

format('hello');       // OK
format(3.14159, 2);    // OK
format(3.14159);       // Error: No overload matches this call.
                       // Overload 2 requires 2 arguments, but got 1.

Or the implementation signature is accidentally exposed:

function process(data: string): number;
function process(data: number): string;
function process(data: string | number): string | number {
  // ...
}

process('test' as string | number);   // Error — implementation signature not callable

Or overloads in a class method conflict:

class Converter {
  convert(value: string): number;
  convert(value: number): string;
  convert(value: string | number): string | number {
    return typeof value === 'string' ? parseInt(value) : String(value);
  }
}

const c = new Converter();
c.convert('42');   // OK
c.convert(42);     // OK
c.convert(someUnion);  // Error: No overload matches this call

Why This Happens

TypeScript function overloads have a counterintuitive rule: the implementation signature is invisible to callers. Only the overload signatures (the ones before the implementation) define what callers can pass.

This design is fundamentally different from how overloading works in other typed languages. In Java and C#, each overload is a separate method with its own body, and the runtime dispatches to the correct one based on argument types. In TypeScript, overloads are purely a compile-time construct. There is only one function body (the implementation), and the overload signatures serve as a type-level filter that restricts which calls are allowed. The implementation signature is the private contract between the overloads and the body; callers never see it.

This distinction causes confusion when developers arrive from C# or Java, where writing void Process(string data) and void Process(int data) creates two independent methods. In TypeScript, those two signatures share one implementation body, and the implementation’s parameter type must be a superset (string | number) of both overloads.

Common mistakes:

  • Implementation signature not covered by overloads — if callers should be able to pass string | number, you need an explicit overload for that union. The implementation’s string | number parameter isn’t callable directly.
  • Missing overloads for valid input combinationsformat(3.14159) (one argument) isn’t covered because the only number overload requires two arguments.
  • Overload signatures incompatible with implementation — TypeScript checks that all overload signatures are assignable to the implementation signature. If they’re not, the implementation can’t handle all cases the overloads promise.
  • Overly narrow implementation signature — the implementation must accept a superset of all overload signatures. If an overload accepts null but the implementation doesn’t, TypeScript errors.

Fix 1: Add Missing Overloads

Add an overload signature for every combination of arguments callers might use:

// WRONG — no overload for format(number) without decimals
function format(value: string): string;
function format(value: number, decimals: number): string;   // Requires 2nd arg
function format(value: string | number, decimals?: number): string {
  if (typeof value === 'string') return value.toUpperCase();
  return value.toFixed(decimals ?? 2);
}

format(3.14);   // Error: No overload matches (number without decimals)

// CORRECT — add the missing overload
function format(value: string): string;
function format(value: number): string;             // Added: number with optional decimals
function format(value: number, decimals: number): string;
function format(value: string | number, decimals?: number): string {
  if (typeof value === 'string') return value.toUpperCase();
  return value.toFixed(decimals ?? 2);
}

format('hello');     // Uses overload 1
format(3.14);        // Uses overload 2
format(3.14, 4);     // Uses overload 3

Order overloads from most specific to least specific:

// BEST PRACTICE — specific overloads first, general last
function createElement(tag: 'canvas'): HTMLCanvasElement;
function createElement(tag: 'img'): HTMLImageElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: string): HTMLElement;   // Catch-all last
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

createElement('canvas').getContext('2d');    // Returns HTMLCanvasElement
createElement('img').src = '/image.png';    // Returns HTMLImageElement
createElement('div').style.color = 'red';   // Returns HTMLElement (catch-all)

Fix 2: Make the Implementation Signature Broad Enough

The implementation signature must be compatible with all overload signatures:

// WRONG — implementation signature too narrow
function greet(name: string): string;
function greet(name: string, greeting: string): string;
function greet(name: string): string {   // Missing 'greeting' parameter
  return `Hello, ${name}`;
}

// CORRECT — implementation matches all overloads
function greet(name: string): string;
function greet(name: string, greeting: string): string;
function greet(name: string, greeting?: string): string {   // Optional second param
  return `${greeting ?? 'Hello'}, ${name}`;
}

// WRONG — return type too specific in implementation
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string {   // Return type doesn't cover number
  // ...
}

// CORRECT — implementation return type covers all overload return types
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {   // Union of all return types
  return typeof input === 'string' ? parseInt(input) : String(input);
}

Fix 3: Use Conditional Types as a Cleaner Alternative

For type-safe overloads based on input type, conditional types can replace overload signatures:

// Overload approach — verbose
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {
  return typeof input === 'string' ? parseInt(input) : String(input);
}

// Conditional type approach — cleaner and handles unions correctly
type ParseResult<T> = T extends string ? number : T extends number ? string : never;

function parse<T extends string | number>(input: T): ParseResult<T> {
  return (typeof input === 'string' ? parseInt(input) : String(input)) as ParseResult<T>;
}

const num = parse('42');        // Type: number
const str = parse(42);          // Type: string
const union = parse(value as string | number);   // Type: number | string — works!

When to use overloads vs conditional types:

  • Use overloads when the logic branches are cleanly separated and you have a small number of combinations
  • Use conditional types when the return type is a mathematical function of the input type
  • Use overloads when different call signatures have very different parameter counts

TS 5.x improvements to overloads: TypeScript 5.0 improved overload resolution by providing better error messages when no overload matches. Instead of just saying “No overload matches this call,” it now shows which overloads were attempted and why each one failed. TypeScript 5.4 further improved inference for calls to overloaded functions in generic contexts, reducing cases where the compiler incorrectly selects a less specific overload.

Fix 4: Fix Class Method Overloads

Class methods follow the same rules as functions:

class HttpClient {
  // Method overloads — define public API
  get(url: string): Promise<unknown>;
  get<T>(url: string): Promise<T>;
  get<T>(url: string, options: RequestInit): Promise<T>;

  // Implementation — must handle all cases
  async get<T = unknown>(url: string, options?: RequestInit): Promise<T> {
    const response = await fetch(url, options);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json() as T;
  }
}

const client = new HttpClient();
client.get('/api/data');                    // Returns Promise<unknown>
client.get<User[]>('/api/users');           // Returns Promise<User[]>
client.get<User>('/api/user', { method: 'POST' });  // Returns Promise<User>

Interface method overloads:

// Define overloads in the interface
interface Parser {
  parse(input: string): number;
  parse(input: number): string;
  parse(input: boolean): string;
}

// Implementation class must satisfy ALL overload signatures
class DataParser implements Parser {
  parse(input: string): number;
  parse(input: number): string;
  parse(input: boolean): string;
  parse(input: string | number | boolean): string | number {
    if (typeof input === 'string') return parseInt(input);
    return String(input);
  }
}

Fix 5: Overloads with Optional Parameters

Common pattern — one required parameter with optional extras:

// Build a query string with optional parameters
function buildQuery(base: string): string;
function buildQuery(base: string, params: Record<string, string>): string;
function buildQuery(base: string, params: Record<string, string>, encode: boolean): string;
function buildQuery(
  base: string,
  params?: Record<string, string>,
  encode = true
): string {
  if (!params) return base;

  const queryString = Object.entries(params)
    .map(([k, v]) => `${k}=${encode ? encodeURIComponent(v) : v}`)
    .join('&');

  return `${base}?${queryString}`;
}

buildQuery('/api/users');
buildQuery('/api/users', { page: '1' });
buildQuery('/api/users', { search: 'hello world' }, false);

Avoid redundant overloads for optional parameters:

// REDUNDANT — these three do the same as one signature with optional params
function greet(name: string): void;
function greet(name: string, age: number): void;
function greet(name: string, age?: number): void { ... }

// SIMPLER — single signature with optional parameter
// (When a function's only variation is optional params, skip overloads)
function greet(name: string, age?: number): void {
  console.log(age ? `${name}, ${age}` : name);
}

Fix 6: Common Overload Anti-Patterns

Anti-pattern: using overloads to simulate union types:

// UNNECESSARY OVERLOAD — just use a union parameter
function log(message: string): void;
function log(message: Error): void;
function log(message: string | Error): void {
  console.log(message instanceof Error ? message.message : message);
}

// SIMPLER — union parameter directly
function log(message: string | Error): void {
  console.log(message instanceof Error ? message.message : message);
}

Anti-pattern: too many overloads instead of generics:

// TOO MANY OVERLOADS — repetitive
function identity(value: string): string;
function identity(value: number): number;
function identity(value: boolean): boolean;
function identity(value: any): any { return value; }

// BETTER — generic preserves type
function identity<T>(value: T): T { return value; }

const s = identity('hello');   // Type: string
const n = identity(42);        // Type: number

When overloads are genuinely needed:

// GOOD USE OF OVERLOADS — different parameter structures, not just types
function on(event: 'click', handler: (e: MouseEvent) => void): void;
function on(event: 'keydown', handler: (e: KeyboardEvent) => void): void;
function on(event: 'scroll', handler: (e: Event) => void): void;
function on(event: string, handler: (e: Event) => void): void {
  document.addEventListener(event, handler);
}

// Type-safe event handler registration
on('click', (e) => {
  console.log(e.clientX, e.clientY);   // e is MouseEvent — correct properties
});

Fix 7: Overloads in Other Languages vs TypeScript

Understanding how other languages handle overloading helps clarify why TypeScript’s approach trips people up.

C# method overloading (runtime dispatch):

In C#, each overload is a separate method with its own body. The CLR dispatches to the correct method at runtime based on argument types. You never write a single implementation that handles all cases:

// C# — each overload is independent
class Formatter {
    public string Format(string value) => value.ToUpper();
    public string Format(int value) => value.ToString();
    public string Format(int value, int decimals) => value.ToString($"F{decimals}");
}
// Caller can pass any matching argument set — no "implementation signature" concept

In TypeScript, you must write one body that handles all overloads. This is fundamentally different and leads to the “No overload matches this call” error when the overload list is incomplete.

Java overloading vs generics:

Java has the same runtime dispatch model as C#. Java developers often use overloading where TypeScript developers should use generics or conditional types. If you’re porting Java code with many overloads to TypeScript, consider whether a generic function or a discriminated union would be more idiomatic.

Flow overloads (different syntax):

Flow uses intersection types for overloads instead of the declaration syntax TypeScript uses:

// Flow — overloads via intersection type
type Format =
  & ((value: string) => string)
  & ((value: number, decimals: number) => string);

const format: Format = (value, decimals) => { /* ... */ };

This is semantically similar to TypeScript overloads but looks different and has slightly different resolution behavior. If you’re migrating from Flow to TypeScript, replace intersection-typed function types with explicit overload declarations.

Fix 8: Debug Overload Resolution

TypeScript tries overloads in order and uses the first match:

// TypeScript checks overloads top-to-bottom, uses first match
function process(input: number): 'number';
function process(input: string | number): 'string-or-number';
function process(input: string): 'string';   // Never reached! string matches overload 2

// process('hello') resolves to overload 2, not overload 3
const result = process('hello');
// Type: 'string-or-number' — not 'string'

// Fix — put more specific overloads first
function process(input: number): 'number';
function process(input: string): 'string';   // More specific — before union
function process(input: string | number): 'string-or-number';
function process(input: string | number): string {
  if (typeof input === 'number') return 'number';
  return 'string';
}

Use @ts-expect-error to test which overload is selected:

function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  return String(value);
}

// Test: ensure format(42) returns string (not number)
const result = format(42);
// Hover over 'result' in IDE to see inferred type

// Test that invalid calls are rejected
// @ts-expect-error — number | string should not match any overload directly
format(Math.random() > 0.5 ? '42' : 42);

Still Not Working?

any in overloads — using any in an overload signature disables type checking for that overload. TypeScript accepts any argument without error. Replace with proper union types.

Declaration merging — function overloads in .d.ts files work through declaration merging. All signatures must appear consecutively (no other declarations between them).

Constructor overloads — class constructors follow the same overload rules as functions:

class Connection {
  constructor(url: string);
  constructor(host: string, port: number);
  constructor(url: string, port?: number) {
    // ...
  }
}

Async overloads — overloaded async functions must have Promise<T> as the return type in both overloads and the implementation:

async function fetchData(id: number): Promise<User>;
async function fetchData(ids: number[]): Promise<User[]>;
async function fetchData(idOrIds: number | number[]): Promise<User | User[]> {
  // ...
}

Overloads with this parameter — TypeScript allows a this parameter in overloads to constrain which object the function can be called on. If you get “The ‘this’ context of type X is not assignable,” check that the this type in the overload matches the implementation’s this type.

Overloads in declaration files break after TypeScript upgrade — TypeScript occasionally tightens overload compatibility checks between minor versions. If a .d.ts file that worked in TS 5.3 fails in TS 5.4, check if the implementation signature is now required to be stricter. Run tsc --noEmit to find which overloads need updating.

Union argument rejected by all overloads — when you pass a union type (string | number) to an overloaded function, TypeScript does not distribute the union across overloads. It checks each overload against the full union and rejects all of them. Use a conditional type generic or add an explicit overload that accepts the union.

For related TypeScript issues, see Fix: TypeScript Mapped Type Error, Fix: TypeScript Declaration File Error, Fix: TypeScript Generic Constraint Error, and Fix: TypeScript Conditional Types Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles