Skip to content

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

FixDevs ·

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.

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

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: 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[]> {
  // ...
}

For related TypeScript issues, see Fix: TypeScript Mapped Type Error and Fix: TypeScript Declaration File Error.

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