Function overloads vs one generic signature — when each wins
Published April 27, 2027
export function parseValue(input: string): string;
export function parseValue(input: number): number;
export function parseValue(input: string | number): string | number {
return typeof input === "string" ? input.trim() : Math.round(input);
}
export const identity = <T extends string | number>(input: T): T => input;
export const trimmed: string = parseValue(" hi ");
export const rounded: number = parseValue(3.7);What
parseValue is declared with two overload signatures (string -> string, number -> number) backed by one implementation. identity does something that looks similar — one signature, T -> T — but is a generic rather than a set of overloads.
Why it matters
Both let you avoid a single (input: string | number): string | number signature, which forces every caller to narrow the result themselves even when they know exactly what they passed in. But overloads and generics solve that for different reasons and don't generalize the same way. Overloads let each input type map to a specifically different output type — useful when the relationship between input and output isn't "the same type back," like a function that takes a string and returns a string, but takes a Date and returns a number. A generic wins when the relationship genuinely is "whatever you give me, you get the same type back" — identity's signature covers every type in string | number (and could cover any type at all, with a broader constraint) with one signature instead of one overload per type.
How it works
parseValue's two overload signatures don't have bodies — they're purely type-level promises about what the function accepts and returns for each case. Only the third, wider signature (string | numberin,string | numberout) has an implementation, and it has to be a superset of every overload it backs.- Inside the implementation,
input's type is the wide union, not whichever overload was called through — thetypeof input === "string"check is still necessary there, same as it would be without overloads at all. identity'sT extends string | numbercaptures the caller's specific type asTand returns that sameT— calling it with astringgives back astring, notstring | number, with no runtime branching needed at all.
Gotchas
- Overloads scale linearly with the number of distinct input/output pairings — three unrelated mappings means three overload signatures to keep in sync with one implementation. Past a handful, a single well-designed generic (or splitting into differently-named functions) usually reads better.
- A generic signature can't express "input type A maps to unrelated output type B" the way overloads can —
identity's shape only works because input and output are structurally the same type. Forcing that mapping into a generic when it doesn't hold means falling back to conditional types, which is usually more complex than the overloads it would replace.
Related
- Conditional types +
infer— one way to make a single generic signature behave like overloads (a different output type depending on the input), without writing multiple signatures. consttype parameters — a different generic-signature concern (preserving literal inference) that doesn't come up with overloads at all, since each overload's return type is fixed at that signature.