Functions and their signatures
A function signature is a promise in two directions: what the caller may pass in, and what the function will hand back.
A promise in two directions
Every function has a signature: the types of what it takes, and the type of what it returns. Writing it down is not ceremony — it is the two facts every caller depends on, said once, checked everywhere.
The parameters say what the function will accept. The return type says what the caller can count on. Between them, they turn "read the whole function to find out" into "read one line".
A signature does not promise that the function is correct, that it finishes, or that it does not throw. It promises the shape of the exchange: values of these types go in, a value of that type comes out. Everything else is the body’s business.
How the promise is checked
When you call add(2, 3), the checker takes the types of the arguments — number and number — and compares them, in order, with the parameter list. Then it gives the call expression the return type from the signature, without looking inside the body. That is the economy of a signature: callers are checked against one line, and the body can be rewritten freely as long as it still keeps the promise.
The body is checked in the other direction. Every return statement is compared with the declared return type, and a path that falls off the end of a function promising number is an error. That error points at the function, not at a call, because the body is the side that broke the promise.
The same function, promised out loud
function add(a: number, b: number): number {
return a + b;
}
function announce(message: string): void {
console.log(message);
}
const double = (value: number): number => value * 2;Nothing to return is worth saying too
void is the return type for a function that answers with nothing — it logs, it saves, it clears. Saying so is what stops the call site from using the result of a function that never had one.
A return type is also a check on you: a function that promises number cannot end with a bare return; on one path and return 5; on another. The compiler walks every path and holds you to the promise.
Optional and default values
A question mark marks a parameter the caller may skip: function greet(who?: string). Inside the function its type is string | undefined — the compiler insists you handle the missing case, which is exactly the bug the annotation just prevented.
A default value makes the same promise but always has something to give: function greet(who = "friend"). The parameter stays a string; the caller may still pass one.
A function type is a shape too
Functions can be passed around, so they need types as well. (n: number) => number describes a function that takes a number and returns one. The arrow is the type-level way of saying "a function from these arguments to this result".
That type is checked where the function is supplied. A callback with the wrong parameter type is refused at the line that passed it, which is the earliest place the mistake could possibly be seen.
What the caller sees
A signature is documentation the compiler enforces. Rename a parameter and no call site changes; change a type or add a required parameter and every caller that no longer fits is reported.
That is why the signature should be settled before the body. The signature is the decision; the body is one implementation of it, and the checker will tell you if the two disagree.
What goes wrong
The optional parameter is where most first errors appear. function greet(who?: string) gives who the type string | undefined, so who.toUpperCase() is refused with Object is possibly ’undefined’. That message is not an obstacle to work around; it is the missing branch made visible. The fix is an if (who) guard or a default value, and both are shorter than the bug report you would otherwise write.
The second error is arity: calling add(2) reports that two arguments were expected. TypeScript refuses both too few and too many arguments, because an argument the function ignores is usually a caller that meant something else. Keep the call and the signature in step, and let the error tell you which is wrong.
Three ways a signature goes wrong. An optional parameter read without handling undefined is the first; the type already warned you. A function typed void whose result is then used is the second. And a callback typed any is the third: it is passed and called, but nothing about the call is checked.
Try these next
- Write
applyTwicethat takes a number and a function, and call it with a doubling function. - Add a default value to an optional parameter and compare the two versions at the call site.
- Give a function an explicit
: voidreturn type, then try to log its result and read the error.
Give a function its signature
Annotate both parameters and the return type of `add`, then call it with two numbers and log the result.
3 left to do
- The first parameter is a number
- The second parameter is a number
- The return type is a number
The parameters carry their types in the parentheses; the return type goes after them, before the opening brace: (a: number, b: number): number.
A function that takes a function
Type the value, the callback and the return type of `applyTwice`, then call it with a doubling function and log the result.
3 left to do
- The value is a number
- The callback is a function that takes and returns a number
- The function returns a number
A function type uses an arrow: step: (value: number) => number. The return type goes after the closing parenthesis, as before.
Write the return type even when the compiler could infer it. On a function, it is the line a caller reads first — and a promise you want checked at the source, not at the call site.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What return type belongs on a function that logs a message and returns nothing?
