Topic 2.1
Typing Functions
In one line
A function has a type too: the kinds of values it takes in and the kind it gives back. Writing those down lets TypeScript check every call and every return.
Think of it like this
A vending machine. It has a slot that only takes coins, and a tray that only gives out a drink. If you push a button with no coin, or try to feed it a banana, it refuses. A typed function is a vending machine: parameters are the slots (what goes in), the return type is the tray (what comes out). The checker is the machine, and it refuses wrong input at the slot, before anything runs.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Function
- A named block of code that takes inputs and usually gives back a result.
- Parameter
- A name for an input, written where the function is defined, like
ainfunction add(a, b). - Argument
- The actual value you pass in when you call the function, like
2inadd(2, 3). - Return value
- What the function hands back with
return. - Signature
- The part of a function that says how to use it: parameter names and types, plus the return type. The body is not part of it.
- Return type
- The
: typeafter the parameter list. TypeScript checks everyreturnagainst it. - Function type expression
- A type that describes a function, like
(n: number) => string. It is written where a type goes. - Arrow function
- The short way to write a function:
(n: number) => n * 2. - Inferred return type
- The return type TypeScript works out from the body when you don't write it.
Step by step
01Parameters and a return type
Annotate each parameter with : type. Add : type after the parentheses to promise what comes out.
From now on add(2, "3") is an error on the argument, and return "oops" inside add is an error on the return.
function add(a: number, b: number): number {
return a + b;
}02Let the return type be inferred
You can leave the return type out. TypeScript reads the return statements and works it out. Hover the function or use // ^? to see it.
For short, private helpers, inference is fine. For functions other people call, writing the return type makes the promise explicit and gives clearer errors.
function shout(text: string) {
return text.toUpperCase() + "!";
}
// TypeScript infers: function shout(text: string): string03Every path must return
When you write a return type, TypeScript checks that every way out of the function gives one. If an if returns but the code can reach the closing } without a return, the function would quietly hand back undefined.
TypeScript reports this as TS2366. Add the missing return (or an else), or widen the return type to include undefined if that is what you mean.
04Arrow functions and function type expressions
An arrow function gets the same annotations. The variable that holds it gets its type inferred: (n: number) => number.
You can write that shape yourself and give it a name. Anything of that type is a function that takes a number and returns text, whatever it's called inside.
type Formatter = (value: number) => string;
const asRupees: Formatter = (value) => "Rs " + value.toFixed(2);05How a call is checked
At each call TypeScript lines the arguments up with the parameters, one by one. The count must match, each argument must fit its parameter's type, and the result has the return type.
Try it yourself
- 1
Fix the missing return
In the third example, predict the fix before you edit: add
return "minor";after theif. Check that the error disappears and the console printsminor 1.Then change the return type of
describetostring | undefinedand remove your fix. The error is gone again, but now every caller must handleundefined. Which version would you ship? - 2
Wrong count of arguments
At the bottom of the first example add
add(1);and thenadd(1, 2, 3);. Predict the two messages, then read them. Is it a TypeScript error to pass too many arguments, too? - 3
Rename a parameter in a type
In the second example, change
(value: number) => stringto(amount: number) => string. Predict whether anything breaks. Nothing does: parameter names in a function type are labels for humans.
Code & diagrams
add has both annotations written. shout has only the parameter, and TypeScript worked out the string return. double is an arrow function whose type was inferred.
Console
(nothing was printed)Expected console output
5 HI! 42The arrows have no annotations: they get value: number and the string return from Formatter. The last line returns a number where Formatter promised a string, so the error points at the body.
Console
(nothing was printed)Expected console output
Rs 1499.00
18%
#5describe promised a string but returns nothing when age is under 18: the console shows undefined. parity has no written return type, so TypeScript made a union of what it can return.
Console
(nothing was printed)Expected console output
undefined 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Pass text to a number parameter
At the bottom of the first example add a function twice (three lines: function twice(n: number): number {, return n * 2;, }) and then, on the line after it, the call twice("4");.
Break #2
Pass the wrong number of arguments
With the same twice added to the first example (and the twice("4"); line removed, so the call goes on line 14), write twice(1, 2); and then twice();.
Myth vs fact
Myth
I have to write the return type on every function.
Fact
No. TypeScript infers it from the body. Write it when you want to state the promise (exported functions, public APIs) or to catch a wrong body early.
Myth
Parameter names are part of a function's type.
Fact
Only the order and types matter. (n: number) => number and (x: number) => number are the same type; the names are documentation.
Myth
Passing extra arguments is fine, like in JavaScript.
Fact
JavaScript allows it, TypeScript does not: Expected 1 arguments, but got 2. (The exception is callbacks: a function that declares fewer parameters than the caller offers is accepted, Topic 2.4.)
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A function declaration is hoisted (usable above its definition), while
const f = () => {}is not. Types don't change that; the checker only warns about use before assignment forconst/let(TS2448). - ▸
TS2366is only reported when the return type doesn't allowundefinedand the end of the function is reachable. Control-flow analysis (Topic 4.3) works out reachability: a function ending inthrow, or whose last statement is an exhaustiveswitch, has no reachable end. - ▸
Since TypeScript 5.1, a function whose return type is
undefinedmay have noreturnstatement at all (function f(): undefined {}is fine). Before that you neededreturn undefined. This matters for typing callbacks that do nothing. - ▸
Return types are one of the cheapest ways to speed up the checker on huge projects: with an explicit return type, TypeScript doesn't need to look inside the body when another file calls the function.
isolatedDeclarations(TypeScript 5.5) goes further and requires them on exports.
Remember this
- 1
The problem this solves: In JavaScript, a function can be called with the wrong kind of input or the wrong number of inputs, and it fails or returns nonsense. Typing the function states what goes in and what comes out. A function is a named block of code that takes some inputs and (usually) gives back a result. The inputs are called parameters when you write the function (
aandbinfunction add(a, b)) and arguments when you call it (2and3inadd(2, 3)). The result you hand back withreturnis the return value. - 2
In TypeScript you annotate each parameter with
: typeafter its name, and you can annotate the return value with: typeafter the closing parenthesis:function add(a: number, b: number): number. This is the signature: the part that says how the function may be used. The body is only how it does it. - 3
Parameters must be annotated; return types usually don't need to be. Without a parameter type, TypeScript has nothing to go on, so in strict mode it reports
TS7006(implicitany, Topic 1.6). The return type, by contrast, is inferred (worked out) from thereturnstatements in the body, and you can see it by hovering or with// ^?. If a function returns different kinds of values, the inferred type is a union of them ("even" | 1, Topic 4.1). - 4
When you do write the return type, you add a second check. TypeScript compares every
returnagainst it, and also checks that every path through the function returns something. If anifhas areturnbut the code can fall off the end, you getTS2366: Function lacks ending return statement and return type does not include 'undefined'. Writing the return type for public functions is a good habit: it states the promise, and a wrong body is blamed instead of every caller. - 5
Arrow functions are the short form:
const double = (n: number) => n * 2. They get the same annotations. The variabledoubledoesn't have a type written on it; TypeScript infers(n: number) => numberfrom the arrow function. That notation,(n: number) => number, is a function type expression: it describes any function that takes a number and returns a number. You can write it yourself wherever a type goes, to name a kind of function:type Formatter = (value: number) => string. - 6
Calls are checked in three ways: the number of arguments (
TS2554: Expected 1 arguments, but got 2), the type of each argument (TS2345), and what you do with the result (a function that returnsstringcan't be used as a number). Parameter names in a function type are only documentation:(n: number) => numberand(x: number) => numberare the same type. What matters is the order and the types. - 7
Where does a function type come from when you never wrote one? Anywhere TypeScript can see the shape. A function declaration, an arrow function, a method: each has a type you can hover. And when a function type is already known (a parameter typed
Formatter), an arrow function you pass in doesn't need annotations: TypeScript fills them in from context. That is contextual typing, the subject of Topic 2.4.
Explain it without notes
What is the difference between a parameter and an argument?
When should you write a function's return type, and when can you leave it out?
What does (a: number, b: number) => string mean, and where can you use it?
What does error TS2366 mean and how do you fix it?
Practice
Write function clamp that takes value, min and max (all numbers) and returns a number: min if value is smaller, max if it is larger, otherwise value. Call it three times and log the results. Then call it with a string and note the error.
Make a type Predicate for functions that take a string and return a boolean. Write two functions of that type, isShort (fewer than 5 characters) and isEmpty, and a function check(text: string, test: Predicate): boolean.
This function should return the grade letter but TypeScript reports TS2366. Fix it without changing its return type: function grade(score: number): string { if (score >= 90) return "A"; if (score >= 75) return "B"; }
Trade-offs
- ↔
An explicit return type states your intent and catches a wrong body early, but it repeats what the body already says and can go stale. A good rule: write it on exports and public methods, infer it on small local helpers.
- ↔
Naming a function type with
type(Formatter) makes signatures short and reusable, at the cost of one more name to look up. Use it when the same shape appears more than once.
Done when you can
I can annotate parameters and a return type, and say what each one promises.
I can read an inferred return type with
// ^?.I can explain and fix
TS2366andTS2554.I can write a function type expression and give it a name.
I can explain why parameter names in a function type do not matter.