AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 09 — Functions: Typing Parameters and Returns

09 — Functions: Typing Parameters and Returns

August 13, 20265 min read
Download as Markdown

"Slap types on the parameters and move on" was my function-typing strategy, and it missed the contract. Writing it down elevated the framing: a function's type is its signature — the types of its parameters and its return — and that signature is a contract the compiler enforces at every call site. [1] Function overloading extends this by letting one function advertise several signatures, so each call shape gets precise types instead of a loose union.

The starting point is that inference doesn't fully cover functions. A function body can infer its return type from what it returns, but parameters have no initial value to infer from — they arrive from the caller. So function signatures are where annotations cluster, by design. Typing a function means declaring what each parameter must be and (optionally, but recommended for public APIs) what the function returns [1]:

function add(a: number, b: number): number {
return a + b;
}

Both parameters are number, the return is number. Every call is now checked: add(1, 2) is fine, add("1", 2) is an error, and the compiler knows the result is a number without me reading the body.

The anatomy of a function type

A function's type can be written standalone — useful for callbacks and reusable signatures:

type MathOp = (a: number, b: number) => number;
const add: MathOp = (a, b) => a + b;

The arrow syntax (a: number, b: number) => number spells out "a function taking two numbers and returning a number." When I assign a concrete function to a variable of that type, the parameter types flow in — add's a and b are inferred as number from the MathOp annotation, no need to re-annotate. This is how callbacks get typed in libraries: a onClick: (event: MouseEvent) => void parameter documents the shape callers must provide.

Optional, default, and rest parameters

Three parameter flavors that come up constantly:

  • Optional — name?: string. The ? marks it as possibly absent; the type inside the function becomes string | undefined.
  • Default — name: string = "Ave". The parameter has a fallback; TypeScript infers the type from the default, and the parameter is effectively optional.
  • Rest — ...nums: number[]. Gathers trailing arguments into an array, typed as an array of the element type.

These cover the realistic shapes functions take. Optional and default parameters must come after required ones, mirroring how JavaScript itself handles them.

Return types: when to annotate

The compiler infers return types from return statements, so for internal helpers I often omit the annotation. For public APIs — library functions, exported utilities — I annotate the return type explicitly. Two reasons: it documents the contract at the signature (callers don't read the body), and it guards against drift. If a refactor accidentally changes what a function returns, an explicit return type catches it at the definition rather than letting every call site silently receive a wider type. The habit is: infer inside, annotate at the boundary.

Function overloading

Function overloading lets one function advertise multiple signatures with different parameter types or counts, so each call shape gets a precise type [2]:

function parse(input: string): object;
function parse(input: string, raw: true): string;
function parse(input: string, raw?: boolean): object | string {
// single implementation
}

The first two lines are overload signatures — what callers see. The third is the implementation signature, which must be compatible with all of them and holds the actual body. When a caller writes parse("{}"), the compiler picks the first matching signature and knows the result is object; parse("{}", true) matches the second and returns string. Without overloads, every call would return the loose object | string union, forcing callers to narrow.

The constraint worth internalizing: the implementation signature isn't visible to callers. Only the overload signatures are. The implementation must accept everything the overloads declare, but its specific shape is private. Overloading is the tool when a function genuinely behaves differently (and returns different types) for different inputs — used sparingly, it makes an API's intent precise; overused, it adds ceremony for little gain.

How I use this

My defaults: always annotate parameter types (inference can't help there); annotate return types on exported and public functions, infer on internal helpers; lean on optional/default/rest for realistic signatures; and reach for overloading only when a function returns meaningfully different types for different inputs and I want callers to get a precise type per call. The discipline that pays off is treating the signature as the contract — the part of the function the rest of the codebase depends on — and making it precise enough that the compiler can catch misuse at every call site.

References

[1] Microsoft, "More on Functions," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/2/functions.html

[2] Microsoft, "Function Overloads," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads

[3] Microsoft, "void," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/2/functions.html#void

Knowledge check · Question 1 of 5

Why do function parameters need explicit type annotations more often than local variables?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!