Chapter 9
Functions: Giving Work a Name
Parameters, return types, and arrow functions.
Three chapters have ended by pointing here. Chapter 3 had you copying variable declarations and changing the names. Chapter 5 had you writing the same two lines per question. Chapter 7 had four pricing rules tangled together where the ordering kept getting harder. All three are the same complaint, and this is the answer.
Naming a piece of work
function greet(personName: string): string {
return `Hello, ${personName}!`;
}
console.log(greet("Ada"));
console.log(greet("Grace"));
Hello, Ada!
Hello, Grace!
Four parts:
function greet, the name you’ll call it by.(personName: string), what it needs, with its type.: string, what it gives back.return, the value that comes out.
personName only exists inside the function. It’s a stand-in, filled with
whatever the caller passed. Call greet twice with different text and you
get two different answers from one piece of code, which is the entire point.
Not everything returns something
A function that only prints has nothing to hand back:
function shout(text: string) {
console.log(text.toUpperCase());
}
shout("hi");
HI
No return, and no return type written. TypeScript works out that this one
returns nothing and calls that void. You can write : void yourself if
you want it stated, and most code doesn’t bother.
TypeScript works out the return type
You don’t have to write : string on greet. TypeScript can see what
comes out:
function area(width: number, height: number) {
return width * height;
}
const wrong: string = area(2, 3);
error TS2322: Type 'number' is not assignable to type 'string'.
It knew area gives back a number without being told, and caught the
mistake at the point of use.
So why write return types at all? Because an annotation is a check, not
a hint. Write : number and TypeScript confirms the body actually delivers
one. Leave it off and whatever the body happens to produce becomes the
answer, including a mistake.
function broken(n: number): number {
console.log(n);
}
error TS2355: A function whose declared type is neither 'undefined',
'void', nor 'any' must return a value.
That’s the annotation earning its place: it caught a missing return.
Without it, this function would have quietly been a void function and
every use of its result would have been the problem instead.
Calling it wrongly
function add(a: number, b: number) {
return a + b;
}
console.log(add(1));
console.log(add(1, 2, 3));
error TS2554: Expected 2 arguments, but got 1.
error TS2554: Expected 2 arguments, but got 3.
Both caught before running. In plain JavaScript the first would have run
with b as undefined and produced NaN, quietly, exactly the kind of
thing chapter 6 warned turns up in your output later looking like an answer.
Arrow functions
There’s a second, shorter way to write one:
const double = (n: number): number => n * 2;
console.log(double(21));
42
An arrow function, stored in a const like any other value. When the
body is a single expression, the braces and the return both disappear,
the value of that expression is what comes back.
For longer bodies you keep the braces, and then you need return again:
const describe = (n: number): string => {
const half = n / 2;
return `${n} is ${half} doubled`;
};
Both forms are ordinary TypeScript and you’ll see plenty of each. This book
uses function for named, standalone jobs and arrows for short ones,
which is roughly what most code does.
Exercise 1 · Pay chapter 5's debt
Chapter 5’s mad-libs ended by noting that every question was the same two lines with a different piece of text. Write
function ask(question: string, answer: string): stringthat prints the question and gives back the answer, then rebuild the program around it.
The reading still happens once, up front, exactly as chapter 5 requires. What the function removes is the repetition around it.
Check yourself
Project
Rules with names
Roughly 45 minutes
Two refactors, both of programs you already have. No new behaviour, which is the point: you’re making working code clearer without changing what it does.
Part one: the ticket pricing from chapter 7.
That project ended with four rules interacting and an ordering problem that got worse as you added a fifth. Split it into named functions:
function basePrice(age: number): number
function ticketPrice(age: number, weekend: boolean): numberbasePrice answers one question, what this age pays before any
surcharge. ticketPrice handles the free case and the weekend
surcharge, and calls basePrice for the part it doesn’t own.
The free-under-5 rule that kept fighting the surcharge stops fighting the moment one function owns “is this free” and returns early. Rules that used to interact by accident now interact on purpose, in one place you can point at.
Check it still works. Run the same boundary ages as chapter 7, 4,
5, 12, 13, 64, 65, on both a weekday and a weekend, and confirm
every answer matches what you got before. A refactor that changes an
answer isn’t a refactor, it’s a bug.
Part two: the guessing game from chapter 8.
Pull the comparison out into a function that turns a guess into a verdict:
function judge(guess: number, secret: number): stringgiving back "too low", "too high" or "right". The loop keeps the
counting and the break, and the deciding moves out.
Now the loop reads like what it does rather than how it does it, and you
can test judge on its own without playing a whole game.
Stretch: write isEven(n: number): boolean and use it to make
chapter 8’s continue example read as if (isEven(i)) continue;. Notice
the function is shorter than its own name and the line still got clearer.
That’s naming doing work, not code reduction.