Grokking TypeScript
Vote

0% completed

​

Functions and Function Types

Adding Types to a Function

What the Compiler Checks

Which Types the Compiler Can Infer

Function Types

In plain JavaScript, addPrices(5, "10") returns the text "510", even though the function was written to add two numbers. The plus sign joins a number and a string as text, and nothing warns the caller.

function addPrices(a, b) { return a + b; } console.log(addPrices(5, "10")); // Output: 510

TypeScript reports this mistake before the program runs, because a function can state what it accepts and what it returns. This chapter builds up those tools with one small example, a shop that calculates prices.

Adding Types to a Function

A type goes after each parameter name, and the return type goes after the closing parenthesis of the parameter list.

function addPrices(a: number, b: number): number { return a + b; } console.log(addPrices(5, "10")); // Error

The compiler reports the wrong argument and gives the reason:

error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

This check happens only at compile time. The types are removed from the JavaScript that tsc writes, and tsc still writes that file when there are type errors unless the noEmitOnError option is set. If you run the emitted file anyway, it prints 510, exactly as the plain JavaScript version does. TypeScript adds no runtime check to the function.

What the Compiler Checks

Once a function has these types, the compiler checks it from two sides. It checks the body against the declared return type, and it checks every call against the parameter list.

The compiler compares each call with the parameter types and compares each returned value with the return type.
The compiler compares each call with the parameter types and compares each returned value with the return type.

In the picture, notice that the two checks are independent. The signature is the part of the function that lists its parameters and its return type. A caller needs only the signature, and the body needs to satisfy only the signature.

The first check looks at the body. A returned value of the wrong type is an error, and so is a path through the body that returns nothing.

function lineTotal(price: number, quantity: number): number { return "free"; // Error } function discountedPrice(price: number): number { // Error if (price > 100) { return price - 10; } }
error TS2322: Type 'string' is not assignable to type 'number'. error TS2366: Function lacks ending return statement and return type does not include 'undefined'.

The second check looks at each call. It compares the type of each argument with the type of its parameter, as you saw with addPrices(5, "10"), and it also compares the number of arguments.

function addPrices(a: number, b: number): number { return a + b; } addPrices(5); // Error addPrices(5, 1, 2); // Error
error TS2554: Expected 2 arguments, but got 1. error TS2554: Expected 2 arguments, but got 3.

JavaScript would accept both calls. The first one would return NaN, and the second one would ignore the third argument and return 6.

Which Types the Compiler Can Infer

You do not have to write every type. The compiler can work out, or infer, a return type from the body. It cannot do this for parameters, because a function body does not decide what callers will pass.

function lineTotal(price: number, quantity: number) { return price * quantity; } const total: number = lineTotal(20, 3); // Allowed: the inferred return type is number console.log(total); // Output: 60 function addPrices(a, b) { // Error on both parameters return a + b; }
error TS7006: Parameter 'a' implicitly has an 'any' type. error TS7006: Parameter 'b' implicitly has an 'any' type.

The any type turns off checking for a value, so the compiler refuses to choose it for you without being told. This rule belongs to strict mode, which is on by default in current TypeScript. Older projects and older compiler versions may have strict mode off, and there an untyped parameter silently becomes any, so check the tsconfig.json of a project you join.

Writing the return type yourself has a cost and a benefit. It takes more characters, but it moves the error to the right place. If a body returns the wrong type by mistake, the error appears on the return line and not at some distant caller. A declared return type is therefore worth writing on functions that other files call, while a short local function can rely on inference.

A function that returns no useful value has the return type void.

function logTotal(total: number): void { console.log(`Total: ${total}`); } logTotal(60); // Output: Total: 60

Function Types

A function is also a value, so a variable can hold one. The type of such a variable is called a function type. It describes the parameters and the return value, and it has no body.

let calculateTotal: (price: number, quantity: number) => number; calculateTotal = (p, q) => p * q; console.log(calculateTotal(5, 3)); // Output: 15

The first line stores no function. It says only that calculateTotal may hold a function that takes two numbers and returns a number. Inside a type, the => separates the parameters from the return type, and it does not create an arrow function.

The compiler then handles the assignment in this order:

  1. It reads the function type on the variable.
  2. It gives p the type number and q the type number, by position, because the assigned function has no types of its own. The term for this is contextual typing.
  3. It infers that p * q is a number and compares that with the declared return type.

The parameter names do not have to match, so p and q are fine even though the type says price and quantity. Only the position and the type of each parameter count.

The compiler rejects a function with the wrong return type, and it also rejects a function that needs more parameters than the type provides.

let calculateTotal: (price: number, quantity: number) => number; calculateTotal = (p, q) => `${p * q}`; // Error calculateTotal = (p: number, q: number, tax: number) => p * q + tax; // Error calculateTotal = (p) => p * 2; // Allowed
error TS2322: Type 'string' is not assignable to type 'number'. error TS2322: Type '(p: number, q: number, tax: number) => number' is not assignable to type '(price: number, quantity: number) => number'. Target signature provides too few arguments. Expected 3 or more, but got 2.

The last line is allowed because a function may ignore arguments that it receives. Callers of calculateTotal will always pass two numbers, and a function that reads only the first one is still safe. The second line is rejected because tax would receive nothing.

Function types are most useful when a function is passed to another function, or when the implementation is chosen while the program runs.

Reading Progress

0%


Vote for new content

On This Page

Adding Types to a Function

What the Compiler Checks

Which Types the Compiler Can Infer

Function Types