Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~27 min· topic 1 of 7

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 a in function add(a, b).
Argument
The actual value you pass in when you call the function, like 2 in add(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 : type after the parameter list. TypeScript checks every return against 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.

add.tswhole filets
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.

shout.tswhole filets
function shout(text: string) {
  return text.toUpperCase() + "!";
}
// TypeScript infers: function shout(text: string): string

03Every 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.

formatter.tswhole filets
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.

How a call is checkeddiagram
Rendering diagram…

Try it yourself

  1. 1

    Fix the missing return

    In the third example, predict the fix before you edit: add return "minor"; after the if. Check that the error disappears and the console prints minor 1.

    Then change the return type of describe to string | undefined and remove your fix. The error is gone again, but now every caller must handle undefined. Which version would you ship?

  2. 2

    Wrong count of arguments

    At the bottom of the first example add add(1); and then add(1, 2, 3);. Predict the two messages, then read them. Is it a TypeScript error to pass too many arguments, too?

  3. 3

    Rename a parameter in a type

    In the second example, change (value: number) => string to (amount: number) => string. Predict whether anything breaks. Nothing does: parameter names in a function type are labels for humans.

Code & diagrams

Parameters, return types and inferred returns

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! 42
A function type expression with a name

The 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%
#5
When a return is missing, and an inferred union

describe 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 1

Break 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");.

terminal
$ Types panel
── what you'll see ──
index.ts(14,7): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

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();.

terminal
$ Types panel
── what you'll see ──
index.ts(14,10): error TS2554: Expected 1 arguments, but got 2.

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 for const/let (TS2448).

  • ▸

    TS2366 is only reported when the return type doesn't allow undefined and the end of the function is reachable. Control-flow analysis (Topic 4.3) works out reachability: a function ending in throw, or whose last statement is an exhaustive switch, has no reachable end.

  • ▸

    Since TypeScript 5.1, a function whose return type is undefined may have no return statement at all (function f(): undefined {} is fine). Before that you needed return 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. 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 (a and b in function add(a, b)) and arguments when you call it (2 and 3 in add(2, 3)). The result you hand back with return is the return value.

  2. 2

    In TypeScript you annotate each parameter with : type after its name, and you can annotate the return value with : type after 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. 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 (implicit any, Topic 1.6). The return type, by contrast, is inferred (worked out) from the return statements 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. 4

    When you do write the return type, you add a second check. TypeScript compares every return against it, and also checks that every path through the function returns something. If an if has a return but the code can fall off the end, you get TS2366: 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. 5

    Arrow functions are the short form: const double = (n: number) => n * 2. They get the same annotations. The variable double doesn't have a type written on it; TypeScript infers (n: number) => number from 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. 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 returns string can't be used as a number). Parameter names in a function type are only documentation: (n: number) => number and (x: number) => number are the same type. What matters is the order and the types.

  7. 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

01

What is the difference between a parameter and an argument?

02

When should you write a function's return type, and when can you leave it out?

03

What does (a: number, b: number) => string mean, and where can you use it?

04

What does error TS2366 mean and how do you fix it?

Practice

01

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.

02

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.

03

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 TS2366 and TS2554.

  • 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.