Function Types in TypeScript
Introduction
Functions are where TypeScript’s type system meets everyday logic: you describe what goes in, what comes out, and which calls are legal. This chapter covers parameter and return types, optional and default parameters, rest parameters, arrow function typing, overloads, and the subtle difference between void and undefined as return types.
Prerequisites
Parameter and Return Type Annotations
function add(a: number, b: number): number {
return a + b;
}
const sum = add(2, 3);Code explanation:
- Each parameter gets a type; the return type after
)documents the result - Callers must pass two numbers; returning a string would fail type-checking
Named function type (for variables or callbacks):
type BinaryOp = (left: number, right: number) => number;
const multiply: BinaryOp = (left, right) => left * right;Code explanation:
BinaryOpdescribes any function with that parameter and return shape- Implementations assigned to
BinaryOpare checked against the alias
Optional Parameters
Use ? for parameters that may be omitted:
function greet(name: string, title?: string): string {
if (title) {
return `Hello, ${title} ${name}`;
}
return `Hello, ${name}`;
}
greet("Ada");
greet("Ada", "Dr.");Code explanation:
title?: stringis equivalent totitle: string | undefinedfor callers- Optional parameters must come after required parameters
Default Parameters
Defaults make parameters optional at call sites and give a value when omitted:
function createUser(name: string, role: string = "member"): { name: string; role: string } {
return { name, role };
}
createUser("Lin");
createUser("Lin", "admin");Code explanation:
roleis optional for callers but always astringinside the function- TypeScript infers
roleasstring, notstring | undefined, when a default exists
Tip
Optional vs Default
Use ? when absence means “no value.” Use = default when absence should mean a specific fallback value.
Rest Parameters
Collect remaining arguments into a typed array:
function sumAll(...values: number[]): number {
return values.reduce((total, n) => total + n, 0);
}
sumAll(1, 2, 3);Code explanation:
...values: number[]must be last in the parameter list- Callers can pass any number of numbers
Tuple rest (fixed prefix + rest):
function logEntry(level: string, ...messages: string[]): void {
messages.forEach((msg) => console.log(`[${level}] ${msg}`));
}Arrow Functions
Expression form with explicit types:
const double = (n: number): number => n * 2;
const names = ["Ada", "Lin"];
const upper = names.map((name: string): string => name.toUpperCase());Often parameter types infer from context:
const lengths = names.map((name) => name.length);name is inferred as string because names is string[].
Arrow function type alias:
type Formatter = (value: string) => string;
const quote: Formatter = (value) => `"${value}"`;void vs undefined as Return Types
| Return type | Meaning | Typical use |
|---|---|---|
void | Caller should ignore the return value | console.log wrappers, event handlers |
undefined | Function explicitly returns undefined | Rare; APIs that must return undefined |
function logLine(text: string): void {
console.log(text);
}
function noOp(): undefined {
return undefined;
}Under strict settings, a function annotated void may return undefined or nothing, but not a meaningful value like number:
function broken(): void {
// return 1; // Error: Type 'number' is not assignable to type 'void'
return;
}Code explanation:
voidmeans “no useful return” for consumers, not “never returns”- Prefer
voidfor side-effect-only functions in application code
Function Overloads
When one function behaves differently by argument shapes, declare multiple call signatures and one implementation:
function formatInput(value: string): string;
function formatInput(value: number): string;
function formatInput(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2);
}
return value.trim();
}
const a = formatInput(3.14159);
const b = formatInput(" hello ");Code explanation:
- Overload signatures are what callers see; the implementation signature must be compatible with all of them
- The implementation body usually uses unions and narrowing (
typeofhere)
Warning
Keep Overloads Small
Many overloads are hard to maintain. Consider separate functions or a single options object when signatures multiply.
Functions as Values: Call Signatures in Types
For object types that are callable:
type Logger = {
(message: string): void;
level: string;
};
const logger: Logger = Object.assign(
(message: string) => {
console.log(message);
},
{ level: "info" }
);Most day-to-day code uses (args) => Return instead; callable object types appear in libraries.
Practical Example: Order Total
Code explanation:
quantity?is optional;?? 1supplies a default at runtime- Rest parameters accept a variable-length cart
Common Mistakes
| Issue | Symptom | Fix |
|---|---|---|
| Optional before required | Compile error | Reorder parameters |
| Wrong return type | Assign result to wrong variable | Match annotation to actual return |
| Overload implementation too narrow | Implementation errors | Widen implementation params with unions |
Using any on parameters | No safety | Type each parameter |
FAQ
Are parameter types required?
With strict / noImplicitAny, yes for functions where types cannot be inferred from context.
Can I use union types on parameters?
Yes. function id(x: string | number) is common; narrow inside the body.
How do I type this in functions?
Use this: SomeType as the first parameter in TypeScript’s fake parameter slot (advanced; classes chapter covers this more).
Is (): void => {} valid?
Prefer (): void => { } or () => undefined for no-op callbacks. Arrow functions returning nothing infer void when used as event handlers.
When should I use overloads vs unions?
Overloads improve autocomplete and documentation when call patterns differ. A single union parameter is simpler when behavior is uniform after narrowing.
Does async change return types?
async functions return Promise<T>. Annotate async function fetch(): Promise<User> (covered more with async JavaScript patterns).