Typing functions
Give function parameters and return values types, make parameters optional, default, or rest, and learn what void means, with calls you can edit and break.
- 01Type parameters and returnsWrite a type on each parameter, and decide when to write the return type or let TypeScript work it out.
- 02Shape how a function is calledUse optional, default, and rest parameters, and read the errors TypeScript gives for a wrong call.
- 03Tell void from undefinedExplain what a
voidreturn type means and why you can't use its result.
A function that says what it needs
A shop page shows prices like $4.00. Here is a small function that does it. It uses toFixed(2), a number method that writes the number with two digits after the point. Text doesn't have that method.
function formatPrice(price) {
return "$" + price.toFixed(2);
}
console.log(formatPrice(4));
console.log(formatPrice("4"));Line 1 makes a function with one parameter, price: the name a value gets inside the function. Line 5 calls it with the argument 4, the value you pass in, and prints $4.00. Line 6 passes the text "4", maybe read from a form. Text has no toFixed, so the code stops with TypeError: price.toFixed is not a function. Nothing warned you before it ran. (The Parameters lesson covers how arguments fill parameters.)
In TypeScript you write a type after each parameter, the same way you write one after a variable. Here is the same code with one type added:
function formatPrice(price: number) { return "$" + price.toFixed(2);} console.log(formatPrice(4));console.log(formatPrice("4"));Line 1 now says price: number: whoever calls formatPrice must pass a number. Line 5 does. Line 6 passes text, so TypeScript marks line 6, on purpose here, because it's the bug:
Argument of type 'string' is not assignable to parameter of type 'number'means "the value you passed in is text, but this parameter only takes a number". "Assignable" means "allowed to be stored there".- The error is on the call, line 6, not inside the function. The function is fine; the call gives it the wrong thing.
price: number is checked at every call. The number 4 fits. The text "4" doesn't, so line 6 gets an error before the code runs.A function's types are a promise in both directions. The parameter types say what every call must pass in. The return type says what every call gets back. This lesson covers both, and the ways to make a parameter optional.
A bakery's order form has a box for each thing it needs, and each box says what goes in it. If you write "two" in the number box, the clerk hands the form back before anyone starts baking. Some boxes are optional, some are already filled in, and the form tells you what you'll get at the end.
- In real life: Each box on the form says what goes in it: "Number of cakes"
- In TypeScript: Each parameter has a type:
quantity: number - In real life: The clerk hands back a form with "two" written in the number box
- In TypeScript: TypeScript marks a call that passes text where a number goes
- In real life: A box marked "(optional)" can stay empty
- In TypeScript: A parameter with
?can be left out - In real life: A box already filled in with "Pick up in store"
- In TypeScript: A default value:
delivery = "pickup" - In real life: The form says what you get: a receipt
- In TypeScript: The return type:
): string
Typing a function means writing a type after each parameter, and, if you want, after the brackets for the value it returns. TypeScript checks every call against the parameter types and every return against the return type, before the code runs.
Types on parameters
Once a parameter has a type, TypeScript checks every call: the kind of each argument, and how many arguments there are. Try it. Each button changes the call on line 5. Watch the list under the code, and the result on the right.
function formatPrice(price: number) { return "$" + price.toFixed(2);} console.log(formatPrice(4));$4.00formatPrice(4)No errorsLine 5 passes the number 4, and price must be a number. TypeScript has nothing to report, and the code prints $4.00.
Press each button. A number has no errors. Text gets the error from the overview, and when it runs the code stops. Text, converted wraps the text in Number(…), which turns it into a number, so the error disappears. Nothing and One too many both get Expected 1 arguments, but got …: TypeScript counts the arguments too. Plain JavaScript never counts; a missing argument is undefined and an extra one is ignored.
What if you leave the type off? TypeScript doesn't look at the calls to guess a parameter's type. It only looks at the function itself, and price on its own could be anything:
function formatPrice(price) { return "$" + price.toFixed(2);}Line 1 has no type on price. TypeScript says Parameter 'price' implicitly has an 'any' type. The type any switches checking off: anything goes in, and nothing is checked. "Implicitly" means you didn't write it; TypeScript would have to pick it for you. Strict mode, which these examples use, turns on a setting called noImplicitAny that makes this an error, so you notice. Here is the same code with that setting off:
function formatPrice(price) { // ^? return "$" + price.toFixed(2);} console.log(formatPrice("4"));The list under the code shows the setting this example changes. The query on line 2 shows (parameter) price: any. There are no errors at all, not even on line 6, which passes text. Run it and it stops with the same TypeError as plain JavaScript. That's why you write a type on every parameter that has no default: it's the one place TypeScript can't work a type out for you.
Parameter types look the same in every kind of function: arrow functions and function expressions too.
const lineTotal = (price: number, quantity: number) => price * quantity; const applyTax = function (total: number, rate: number) { return total + total * rate;}; console.log(lineTotal(3, 2));console.log(applyTax(10, 0.5));Line 1 is an arrow function with two number parameters. Line 3 stores a function expression in applyTax, with its types in the same place: after each parameter name. Line 7 prints 6, and line 8 prints 15: 10 plus half of 10.
Return types: worked out or written
You usually don't have to write what a function returns. TypeScript reads the return lines and works it out, which is called inference:
function lineTotal(price: number, quantity: number) {// ^? return price * quantity;} const total = lineTotal(3, 2);// ^?console.log(total);Line 1 types the two parameters and nothing else. The query on line 2 points at lineTotal and shows its whole signature, the list of what goes in and what comes out: function lineTotal(price: number, quantity: number): number. The : number at the end is the return type TypeScript worked out, because a number times a number is a number. Line 6 calls it, the query on line 7 shows const total: number, and line 8 prints 6.
You can also write the return type yourself, after the closing bracket of the parameters. Then TypeScript checks every return line against it:
function cartLabel(count: number): string { if (count === 1) return "1 item"; return count + " items";} console.log(cartLabel(1));console.log(cartLabel(3));Line 1 says cartLabel takes a number and returns a string. Lines 2 and 3 both return text, so there are no errors. Line 6 prints 1 item and line 7 prints 3 items.
Why write it, if TypeScript can work it out? Because it changes where a mistake gets reported. In the next playground, line 2 has a mistake: free shipping returns the text "free" instead of a number. Switch the return type on and off, and watch where the error lands.
function shippingCost(total: number) { if (total > 50) return "free"; return 5;} const cost = shippingCost(60);// ^?console.log(cost + 10);free10const cost: "free" | 51 error: line 8With no return type, TypeScript works one out from the two return lines: "free" | 5, the text free or the number 5. The function has no error. The mistake shows up on line 8, where cost + 10 gets Operator '+' cannot be applied to types 'string | number' and 'number'. The message names the wider types: cost may be text or a number, so TypeScript can't tell whether + should add or join. The code prints free10.
With No return type, the query on line 7 shows const cost: "free" | 5, a union: cost is either the exact text "free" or the exact number 5. The function itself looks fine to TypeScript, and the error appears on line 8, where the result is used. Its message names the wider types, string | number: cost may be text or a number, so TypeScript can't tell whether + should add or join. With Return type: number, the error moves to line 2, right on the mistake. Fixed: return 0 removes it, and the code prints 10.
return "free", in both. With no return type, TypeScript reports it where the result is used, on line 8. With : number, it reports the return line itself, line 2.A written return type also catches a path that forgets to return:
function discountFor(code: string): number { if (code === "SAVE10") return 10;}Line 1 promises a number. Line 2 returns 10 for one coupon code, but for any other code the function reaches its end without a return, and JavaScript gives back undefined. TypeScript says Function lacks ending return statement and return type does not include 'undefined': "some path reaches the end, so it would return undefined, and your return type doesn't allow that". Adding return 0; before the closing brace fixes it.
The table below compares the two choices. Read each row as a question, then the answer for each choice.
| Question | Left out | Written, like : number |
|---|---|---|
| Who decides the type? | TypeScript, from your return lines. | You do. TypeScript checks each return against it. |
Where does a wrong return get reported? | Often where the result is used, far from the mistake. | On the return line itself. |
| Does it change what runs? | No. Types are removed before the code runs. | No. Types are removed before the code runs. |
| When is it a good choice? | Small functions whose result is obvious, like price * quantity. | Functions other files use, and ones with several return lines. |
Optional, default, and rest parameters
By default every parameter is required. Three kinds of parameter let a call pass fewer or more arguments. The first is an optional parameter: put ? after its name.
function greet(name: string, title?: string) { // ^? return "Hello, " + (title ? title + " " : "") + name;} console.log(greet("Asha"));console.log(greet("Asha", "Dr."));Line 1 says title?: string, so a call may leave it out. The query on line 2 shows its type inside the function: string | undefined, "a string, or undefined when it was left out". Line 3 checks title before using it. Line 6 leaves it out and prints Hello, Asha; line 7 passes one and prints Hello, Dr. Asha.
If the function forgets to check, TypeScript points at the place it could go wrong:
function greet(name: string, title?: string) { return "Hello, " + title.toUpperCase() + " " + name;}Line 2 calls title.toUpperCase(). TypeScript says 'title' is possibly 'undefined': when a call leaves the title out, this line would throw a TypeError.
The second kind is a default parameter: = value after the name. A call may leave it out, and then it gets the default. You usually don't write its type; TypeScript works it out from the default.
function shippingFee(total: number, fee = 5) { // ^? return total + fee;} console.log(shippingFee(20));console.log(shippingFee(20, 0));console.log(shippingFee(20, undefined));Line 1 gives fee the default 5, and the query on line 2 shows (parameter) fee: number: inside the function it's always a number, never undefined. Line 6 leaves it out and prints 25. Line 7 passes 0, which is a real value, and prints 20. Line 8 passes undefined, which counts as leaving it out, and prints 25.
function shippingFee(total: number, fee = 5) {// ^? return total + fee;}The query on line 2 shows the signature callers see: function shippingFee(total: number, fee?: number): number. From outside, a default parameter is just an optional one; only the function knows what it falls back to.
Optional and default parameters go after the required ones. TypeScript won't let a required parameter follow an optional one:
function addToCart(note?: string, item: string) { return item;}Line 1 puts the optional note first. TypeScript says A required parameter cannot follow an optional parameter: arguments fill parameters from left to right, so there'd be no way to pass item without passing a note first. Swap them to (item: string, note?: string).
The third kind is a rest parameter: ... before the name collects every remaining argument into one array (the Spread & rest lesson covers it in JavaScript). Its type is an array type, like number[], "an array of numbers".
function cartTotal(...prices: number[]) { // ^? let total = 0; for (const price of prices) total += price; return total;} console.log(cartTotal(3, 4, 5));console.log(cartTotal());const saved = [2, 2];console.log(cartTotal(...saved));console.log(cartTotal(3, "4"));Line 1 says ...prices: number[], and the query on line 2 confirms (parameter) prices: number[]. Line 8 passes three numbers and prints 12. Line 9 passes none, so prices is an empty array and it prints 0. Line 11 spreads an array into the call and prints 4. Line 12 is the error on purpose: every argument must be a number, and "4" is text. Run it anyway, and 3 + "4" joins into 34.
Now try all three kinds together. orderSummary has a required item, a quantity with the default 1, and an optional note. Type different arguments on line 6:
function orderSummary(item: string, quantity = 1, note?: string) { const line = quantity + " x " + item; return note ? line + " (" + note + ")" : line;} console.log(orderSummary("Tea"));"Tea"No errorsprints 1 x TeaOne argument fills item. quantity is left out, so it gets its default, 1. note is left out too, so it's undefined and the line has no note.
With "Tea", quantity uses its default and note is left out. Passing undefined as the second argument also uses the default, so you can still pass a note. An empty box gets Expected 1-3 arguments, but got 0: one argument is required, and up to three are allowed.
orderSummary("Tea") passes one argument. item gets it, quantity gets its default 1, and the optional note is undefined.The table below lists the kinds of parameter. Read each row as: this parameter, can a call leave it out, and its type inside the function.
| When you write | Can a call leave it out? | Its type inside the function |
|---|---|---|
item: string | No. Every call must pass it. | string |
note?: string | Yes. Then it's undefined. | string | undefined, so check it before you use it as a string. |
quantity = 1 | Yes. Then it's 1, and passing undefined gives 1 too. | number, worked out from the default. |
...prices: number[] | Yes. Then it's an empty array. | number[], an array of every extra argument. |
title: string | undefined | No. The caller must pass something, even if it's undefined. | string | undefined |
void: when nothing comes back
Some functions do a job, like printing or saving, and give nothing back. In JavaScript such a call still gives back a value: undefined. TypeScript gives these functions a special return type, void:
function logOrder(id: number) { console.log("Order " + id);} const result = logOrder(7);// ^?console.log(result);logOrder has no return. Line 5 keeps what the call gives back, and the query on line 6 shows const result: void. void means "this function gives back nothing you should use". When it runs, line 5 prints Order 7 and line 7 prints undefined, because that's what JavaScript returns.
logOrder(7) does its job: it prints Order 7. What the call gives back has the type void, which means "nothing for you to use".Because a void result isn't meant to be used, TypeScript stops you from relying on it:
function logOrder(id: number): void { console.log("Order " + id);} if (logOrder(7)) { console.log("Logged");}Line 1 writes the return type void itself. Line 5 asks whether the result is true, as if the function reported success. TypeScript says An expression of type 'void' cannot be tested for truthiness: the result is always undefined, so the if would never run. When it runs, it prints Order 7 and never Logged.
function logOrder(id: number): void { console.log("Order " + id); return id;}A function you write as : void must not return a value. Line 3 returns id, and TypeScript says Type 'number' is not assignable to type 'void'.
undefined is different. It's a real value a caller may use. Write it in a return type when "nothing found" is a result the caller should handle:
function findCoupon(code: string): number | undefined { if (code === "SAVE10") return 10;} const discount = findCoupon("HELLO");// ^?console.log(discount ?? 0);Line 1 says findCoupon returns a number, or undefined when the code isn't known. Because the return type includes undefined, falling off the end after line 2 is allowed. The query on line 6 shows const discount: number | undefined, and line 7 uses it: ?? gives 0 when the left side is undefined, so it prints 0.
The table below compares the two. Read each row as a question, then the answer for each type.
| Question | void | undefined |
|---|---|---|
| What it means | The caller should ignore the result. | The result is the value undefined, and the caller may use it. |
What a function with no return gets | void, worked out by TypeScript. | Only if you write : undefined yourself. |
Can you test the result in an if? | No: An expression of type 'void' cannot be tested for truthiness. | Yes. It's an ordinary value. |
Can the function return 7? | No: Type 'number' is not assignable to type 'void'. | No: 7 isn't undefined. |
Typing a checkout's helper functions
Here is the kind of code a real checkout page has: a few small functions that work out and show a total. Each one uses something from this lesson.
type CartItem = { name: string; price: number; quantity: number }; function lineTotal(item: CartItem): number { return item.price * item.quantity;} function cartTotal(...items: CartItem[]): number { let total = 0; for (const item of items) total += lineTotal(item); return total;} function formatPrice(amount: number, currency = "$"): string { return currency + amount.toFixed(2);} function logCheckout(total: number, coupon?: string): void { const note = coupon ? " with " + coupon : ""; console.log("Checkout: " + formatPrice(total) + note);} const tea: CartItem = { name: "Tea", price: 3, quantity: 2 };const cake: CartItem = { name: "Cake", price: 4, quantity: 1 };logCheckout(cartTotal(tea, cake));logCheckout(cartTotal(cake), "SAVE10");Line 1 is a type alias: type CartItem = … names the shape every cart item has. Line 3 takes one item and returns a number. Line 7 takes any number of items with a rest parameter. Line 13 has a default currency, so callers usually pass only the amount. Line 17 has an optional coupon and returns void, because it only prints. Line 24 prints Checkout: $10.00 (two teas at 3 and a cake at 4), and line 25 prints Checkout: $4.00 with SAVE10.
Notice what you didn't have to write: no types on total or note, and no type on currency. TypeScript works those out. The return types are written because other parts of the page call these functions, and a mistake inside one should be reported there, not at every call.
function double(n: number) { return n * 2; } double("4");function greet(name?: string) { return name; } greet();function add(a: number, b: number) { return a + b; } add(1);function fee(total: number, rate = 5) { return total + rate; } fee(10);function sum(...n: number[]) { return n.length; } sum(1, 2, 3);function log(msg: string): void { return msg; }function size(n: number): string { return n; }function tip(total: number, pct?: number) { return total * (pct ?? 10) / 100; } tip(20, undefined);
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
An optional parameter and a parameter whose type includes undefined look alike inside the function. They aren't the same for callers:
function greet(name: string, title: string | undefined) { return title ? title + " " + name : name;} console.log(greet("Asha", undefined));console.log(greet("Asha"));Line 1 says title: string | undefined, with no ?. Line 5 passes undefined and prints Asha. Line 6 leaves it out, and TypeScript says Expected 2 arguments, but got 1. The type allows undefined; only ? lets a call leave the argument out.
- "TypeScript checks the arguments while the code runs." No. It checks the calls before the code runs. While it runs, the types are gone, and
formatPrice("4")still throws. - "I have to write the return type on every function." No. TypeScript works it out from the
returnlines. Write it when you want mistakes reported inside the function. - "TypeScript can work out a parameter's type from the calls." No. It never looks at the calls, so a parameter with no type and no default is
any, which strict mode reports. (A function you pass to another function is different: Function types shows how it gets its types from where it goes.) - "
note?: stringandnote: string | undefinedare the same." No. Only?lets a call leave the argument out. Inside the function, both arestring | undefined. - "Extra arguments are fine, like in JavaScript." No. TypeScript counts them:
Expected 1 arguments, but got 2. Use a rest parameter if a function really takes any number. - "
voidis just another name forundefined." No.undefinedis a value you can use.voidmeans the caller shouldn't use the result at all.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
Two calls, one with a mistake. Which line will TypeScript mark?
function applyDiscount(total: number, percent: number) {
return total - (total * percent) / 100;
}
console.log(applyDiscount(20, 10));
console.log(applyDiscount(20, "10"));function applyDiscount(total: number, percent: number) { return total - (total * percent) / 100;} console.log(applyDiscount(20, 10));console.log(applyDiscount(20, "10")); // line 6: "10" is textLine 6 passes "10", which is text, where percent must be a number, so TypeScript reports Argument of type 'string' is not assignable to parameter of type 'number' there.
No return type is written. What type does TypeScript give ready? Guess first. Then point at ready to check, or press Edit & run and add a // ^? line under it.
function hasItems(cart: string[]) { return cart.length > 0;} const ready = hasItems(["Tea"]);function hasItems(cart: string[]) { return cart.length > 0;} const ready = hasItems(["Tea"]);// ^?cart.length > 0 is a comparison, which gives true or false. TypeScript works out that hasItems returns a boolean, so the query on line 6 shows const ready: boolean.
TypeScript marks line 1 twice. Give both parameters a type. What does the fixed code print?
function lineTotal(price, quantity) { return price * quantity;} console.log(lineTotal(3, 2));function lineTotal(price: number, quantity: number) { return price * quantity;} console.log(lineTotal(3, 2));With price: number and quantity: number, both errors are gone, TypeScript works out that the function returns a number, and line 5 prints 6.
The function already handles a missing title on line 2, but TypeScript marks the call on line 5. Change line 1 so the call is allowed. What does it print?
function greet(name: string, title: string) { return title ? title + " " + name : name;} console.log(greet("Asha"));function greet(name: string, title?: string) { return title ? title + " " + name : name;} console.log(greet("Asha"));title?: string lets a call leave the title out. Inside, title is undefined, so line 2 returns just the name, and the code prints Asha.
TypeScript says Rest parameter 'tags' implicitly has an 'any[]' type. Give tags a type. What type did you write?
function tagList(label: string, ...tags) { return label + ": " + tags.join(", ");} console.log(tagList("Tea", "hot", "large"));function tagList(label: string, ...tags: string[]) { return label + ": " + tags.join(", ");} console.log(tagList("Tea", "hot", "large"));A rest parameter's type is an array type. ...tags: string[] says every extra argument is a string. The error is gone, and the code prints Tea: hot, large.
TypeScript marks line 1 of this shipping function. What does the starter code print when it runs? Then fix it so every country gets a number.
function shippingFor(country: string): number { if (country === "IN") return 0; if (country === "US") return 5;} console.log(shippingFor("FR"));function shippingFor(country: string): number { if (country === "IN") return 0; if (country === "US") return 5; return 10;} console.log(shippingFor("FR"));For "FR" neither if matches, so the starter reaches the end and gives back undefined, which it prints. A final return 10; covers every other country, the error is gone, and it prints 10.
Check your understanding
For each question, think about what TypeScript checks: the arguments at each call, and the return lines against the return type. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
price: numberinfunction formatPrice(price: number)do?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in these two
lineTotalcalls?Read the code, then predictfunction lineTotal(price: number, quantity: number) { return price * quantity; } console.log(lineTotal(3, 2)); console.log(lineTotal(3));Choose an answer to see the explanation.
Question 3 of 7No return type is written. What type does TypeScript give
labelon line 5?Read the code, then predictfunction cartLabel(count: number) { return count + " items"; } const label = cartLabel(3);Choose an answer to see the explanation.
Question 4 of 7Inside
addNote, what is the type ofnote?Read the code, then predictfunction addNote(item: string, note?: string) { return note ? item + ": " + note : item; }Choose an answer to see the explanation.
Question 5 of 7What does this call with
undefinedfor the fee print?Read the code, then predictfunction shippingFee(total: number, fee = 5) { return total + fee; } console.log(shippingFee(20, undefined));Choose an answer to see the explanation.
Question 6 of 7Which line does TypeScript flag among these
sumcalls?Read the code, then predictfunction sum(...prices: number[]) { return prices.length; } sum(); sum(1, 2, 3); sum(1, "2");Choose an answer to see the explanation.
Question 7 of 7A function's return type is
void. What should you do with what a call gives back?Choose an answer to see the explanation.
Key takeaways
- Write a type after every parameter that has no default:
price: number. TypeScript can't work a parameter's type out from the calls. - TypeScript checks each call: the kind of each argument, and how many there are.
- The return type is worked out from the
returnlines. Write it, like): number, to have mistakes reported inside the function. name?: stringmay be left out, and isstring | undefinedinside.fee = 5may be left out, and is always a number inside.- A rest parameter,
...prices: number[], takes any number of arguments as an array. voidmeans "nothing for you to use".undefinedis a value a caller can use.
Remember the one-liner.
Parameter types are checked at every call, and the return type is checked at every return.
Coming next: Function types. It shows how to describe a function itself as a type, so you can type callbacks and event handlers.