A function sometimes accepts several distinct call shapes and returns a different type for each. TypeScript expresses this with overload signatures — several declarations with no body — followed by one implementation signature. Callers see the overloads; the engine picks the first signature that matches, top to bottom.
Before you start
You should be comfortable with function types, union types and generic constraints. This article is about call-shape modeling; it does not cover ThisParameterType or advanced generic inference.
Step-by-step walkthrough
Step 1: List the specific signatures first
Resolution is ordered, so put the narrow signatures before the broad ones. If (value: string) comes before (value: string | number), a string argument matches the first. Reordering the declarations can change which return type a caller sees even though the implementation never changes.
Step 2: Keep one compatible implementation signature
The implementation signature must be compatible with every overload, but callers can never call it directly. A common bug is making the implementation signature too specific, so it no longer covers all the overloads; the compiler then reports that a signature is not assignable. Widen the implementation parameters and narrow with runtime checks inside the body.
Step 3: Choose overloads or a union return deliberately
Overloads are worth it when the call shapes genuinely differ (different argument counts or literal discriminants). When the only difference is the return type based on an argument’s type, a single generic or conditional return type is usually clearer and avoids resolution-order surprises.
Worked scenario
The two overloads produce different return types for the same function name.
function parse(value: string): number;
function parse(value: number): string;
function parse(value: string | number): number | string {
return typeof value === 'string' ? value.length : String(value);
}
const length = parse('abc'); // number
const text = parse(12); // stringWalk through the example
parse('abc') matches the first overload and is typed number. parse(12) skips the first signature and matches the second, so it is typed string. The implementation signature is broader than either overload and is invisible at call sites: writing parse(true) is an error even though the implementation’s string | number parameter would not have accepted it anyway.
Common mistake
Writing overloads whose signatures overlap in a surprising order, then wondering why the wrong return type appears. A related mistake is putting application logic in a signature: overloads describe shapes only. Finally, do not reach for overloads when a single signature with a conditional return type (or a literal-typed parameter) would read better and produce the same errors.
Verify the behavior
Assign each call to a typed variable and confirm the inferred types differ (const a: number = parse('x')). Call with a value that matches no overload and check the compiler rejects it. Reorder the overloads and observe that a call matching both now resolves to the first one, which demonstrates that order is part of the API.
Interview exercise
You have get(key: string) and get(keys: string[]). Should these be two overloads or two differently named methods?
Answer and reasoning
Two overloads are reasonable because the argument shapes differ (single value versus list) and the return types differ (Value versus Map<string, Value>). The overloads keep one familiar name while making the list form explicit. If the two behaviors were conceptually different operations, distinct names would be clearer; overloads should express the same operation with different inputs, not unrelated commands.
Follow-up discussion
How many overloads is too many? When the list becomes hard to read, the function is probably doing several jobs; split it. Can overloads be generic? Yes, and a generic overload often replaces several concrete ones, provided the relationship between parameters and the return type can be expressed.
Continue learning
Compare this with conditional types and type versus interface when deciding how to model a callable shape. Read the TypeScript function handbook and take the TypeScript MCQs.