---
title: "09 — Functions: Typing Parameters and Returns"
uid: typescript-functions
tags: ["signatures", "typescript", "roadmap:typescript", "function-overloading", "functions"]
excerpt: "A function's type is its signature — parameters and return — enforced at every call site. Overloading lets one function advertise several signatures for precise types per call."
date: 2026-08-13T03:27:28+0000
source: https://www.aveshina.my.id/en/blog/typescript-functions
---

"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](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](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](https://www.typescriptlang.org/docs/handbook/2/functions.html#void)

```quiz
Q: Why do function parameters need explicit type annotations more often than local variables?
- because parameters have no initial value for inference to derive a type from
- because parameters are always any by default regardless of strict mode
correct: 0
explain: Local variables infer from their initial value. Parameters arrive from the caller with no initial value, so there's nothing to infer from — annotations (or a contextual type) supply the type.

Q: A standalone function type like `(a: number, b: number) => number` is useful for…
- typing callbacks and reusable signatures
- nothing — only inline annotations work
correct: 0
explain: A named function type (often via type alias) documents the shape of callbacks and reusable signatures, so variables and parameters can reference it.

Q: You annotate the return type on exported library functions but infer it on internal helpers. Why?
- there's no reason; always infer
- explicit return types document the contract at the boundary and catch drift during refactors
correct: 1
explain: For public APIs, an explicit return type is documentation and a guard against accidental changes. Inside function bodies, inference is precise enough that annotation is noise.

Q: Function overloading lets one function…
- have multiple bodies selected at runtime
- advertise multiple signatures so each call shape gets a precise type
correct: 1
explain: Overloads are multiple call signatures backed by one implementation. Callers see the precise signature that matches their arguments; the single implementation body handles all cases.

Q: In an overloaded function, which signature(s) do callers see?
- only the overload signatures — the implementation signature is private
- only the implementation signature
correct: 0
explain: The overload signatures are the public contract. The implementation signature must be compatible with them but isn't visible to callers, who match against the overloads only.
```
