Lesson 3 • TypeScript
Functions 🔧
By the end of this lesson you'll be able to type a function's parameters and return value, make arguments optional or give them defaults, accept any number of arguments, describe a function as a type, and write overloads — so TypeScript catches a wrong call before your code ever runs.
What You'll Learn in This Lesson
- • Type function parameters and return values (and why that stops bugs)
- • Make a parameter optional with
?or give it a default value - • Use
voidfor functions that return nothing - • Accept any number of arguments with rest parameters
...args - • Describe a function's shape with a function type expression and write arrow functions
- • Define multiple typed call signatures with function overloads
let age: number = 25;. Here you'll apply that same idea to functions.<pre> blocks show the exact TS syntax. Install TypeScript to get full type checking in your own editor.💡 Real-World Analogy
A typed function is like a vending machine. The coin slot only accepts the right shape (the parameter types), and the tray always delivers the kind of item you expect (the return type). Put in the wrong coin and the machine rejects it at the slot — it doesn't take your money, jam, and break halfway through. TypeScript is that coin slot: it rejects a bad call at the door, before the machine ever runs, instead of crashing mid-purchase like plain JavaScript would.
1. Typing Parameters & Return Values
In TypeScript you add a type annotation after each parameter and after the parentheses for the return value. The shape is always function name(param: Type): ReturnType. This is the single most useful thing TypeScript adds to JavaScript: call the function with the wrong kind of value and you get a red squiggle in your editor, not a 2am production crash.
// TypeScript — parameter types in green, return type after the )
function add(a: number, b: number): number {
return a + b;
}
function shout(text: string): string {
return text.toUpperCase() + "!";
}
// "void" = this function returns NOTHING useful (it just does something).
function log(message: string): void {
console.log("[LOG]", message);
}A return type of void means "this function doesn't hand a value back" — you call it for its effect (printing, saving) not its result. Run the worked example below; it's the same code with annotations removed so it executes as JavaScript.
Worked example: parameters, returns & void
Read every comment, run it, and check the output matches.
// In real TS you write the TYPES; here we run the JS so you see the result.
// TS signature: function add(a: number, b: number): number
// - a and b MUST be numbers
// - the function MUST return a number
function add(a, b) {
return a + b;
}
console.log("add(5, 3) =", add(5, 3)); // 8
console.log("add(10, 20) =", add(10, 20)); // 30
// TS signature: function shout(text: string): string
function shout(text) {
return text.toUpperCase() + "!";
}
console.log("shout('hi') =", sh
...2. Optional & Default Parameters
Sometimes a parameter isn't always needed. Add ? after its name to make it optional — TypeScript then treats it as possibly undefined, so you must handle that case. Or give it a default value with = value: if the caller leaves it out, the default is used automatically. A default also makes the parameter optional, but you never have to check for undefined.
// Optional parameter — "greeting" may be undefined, so guard it:
function greet(name: string, greeting?: string): string {
return (greeting ?? "Hello") + ", " + name + "!";
}
// Default parameter — cleaner: no undefined to worry about:
function greet2(name: string, greeting: string = "Hello"): string {
return greeting + ", " + name + "!";
}
// Rule: optional/default parameters must come AFTER required ones.Now you try. The program below is almost complete — fill in the two blanks marked ___ using the hints, then run it and check your output.
🎯 Your turn: defaults in action
Fill in the ___ blanks, then check your output against the expected lines.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// TS signature: function greet(name: string, greeting: string = "Hello"): string
// "greeting" has a DEFAULT, so callers can leave it out.
function greet(name, greeting) {
// 1) If greeting is missing, fall back to "Hello".
// (In TS the "= 'Hello'" default does this for you automatically.)
if (greeting === undefined) greeting = ___; // 👉 the text "Hello" in quotes
// 2) Build and return the message: greeting + ",
...3. Rest Params, Arrows & Function Types
A rest parameter (...numbers: number[]) collects "any number of" trailing arguments into a typed array — perfect when you don't know how many you'll get. An arrow function is a compact way to write a function: (n: number): number => n * 2. And a function type expression lets you describe a function's shape as a reusable type, so you can pass functions around with full type safety.
// Rest parameter — the array type goes on the ...name:
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
// Arrow function with types:
const double = (n: number): number => n * 2;
// Function TYPE EXPRESSION — name a function shape and reuse it:
type MathOp = (a: number, b: number) => number;
const multiply: MathOp = (a, b) => a * b; // a, b inferred as number
const subtract: MathOp = (a, b) => a - b;
// Now a function can accept any MathOp:
function applyOp(a: number, b: number, op: MathOp): number {
return op(a, b);
}Read the worked example, then run it. Notice how applyOp takes a function as an argument — that's only safe because MathOp pins down exactly what that function must look like.
Worked example: rest, arrows & function types
See rest parameters, arrow functions, and a passed-in function type.
// REST PARAMETERS — collect "any number of" args into an array.
// TS signature: function sum(...numbers: number[]): number
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log("sum(1, 2, 3) =", sum(1, 2, 3)); // 6
console.log("sum(10, 20, 30, 40) =", sum(10, 20, 30, 40)); // 100
console.log("sum() =", sum()); // 0
// ARROW FUNCTIONS — a shorter way to write a function.
// TS signature: const double = (n: numb
...Your turn again — finish the average function. It uses a rest parameter and an arrow inside reduce. Fill in the two blanks:
🎯 Your turn: average with a rest param
Fill in the starting total and the final calculation, then run it.
// 🎯 YOUR TURN — fill in the blanks to finish "average".
// TS signature: function average(...scores: number[]): number
function average(...scores) {
// 1) Add up every score. Use reduce with an arrow function.
const total = scores.reduce((sum, n) => sum + n, ___); // 👉 the starting total, 0
// 2) Guard against divide-by-zero, then return the mean.
if (scores.length === 0) return 0;
return ___; // 👉 total / scores.length
}
console.lo
...4. Function Overloads
An overload lets one function advertise several different typed call signatures. You write the signatures first (no body), then one implementation that handles them all. Callers only ever see the clean, specific signatures — TypeScript hides the messy string | number implementation underneath. This is how you say "this function behaves differently depending on what you give it" while keeping every call fully type-checked.
// Two overload signatures (no body) — the public contract:
function format(value: string): string;
function format(value: number): string;
// One implementation signature (must be compatible with both):
function format(value: string | number): string {
if (typeof value === "string") {
return value.trim().toUpperCase();
}
return value.toFixed(2);
}
format("hi"); // OK → string
format(3.14159); // OK → string
// format(true); // ❌ Error: no overload matches a booleanRun the worked version below. The behaviour branches on typeof value at runtime, while the overload signatures give callers precise, safe types at compile time.
Worked example: function overloads
One function, two typed signatures — string and number both return a string.
// FUNCTION OVERLOADS — one function, several typed call signatures.
// TS:
// function format(value: string): string; // overload 1
// function format(value: number): string; // overload 2
// function format(value: string | number): string { // implementation
// ...
// }
// Callers see ONLY the two clean signatures, not the messy implementation.
function format(value) {
if (typeof value === "string") {
return value.trim().toUpperCase(); // string branch
}
retu
...🔎 Deep Dive: arrow functions vs function
Arrow functions aren't just "shorter function". The key difference is this: an arrow function does not create its own this — it borrows the one from where it was written. That makes arrows the right choice for callbacks (like inside map, filter, reduce, or event handlers) where you want to keep the surrounding this.
For a one-line return you can drop the braces and the word return: (n) => n * 2. If you need braces for multiple statements, you must write return yourself.
const double = (n: number): number => n * 2; // implicit return
const greet = (name: string): string => { // braces → explicit return
const msg = "Hi " + name;
return msg;
};Putting It Together: a Pricing Function
This small function uses everything from the lesson at once: a required parameter, a default parameter, a rest parameter, and an explicit string return type. Read it line by line — you understand every part now.
Worked example: flexible price()
Required base, default tax rate, and any number of extra fees.
// === A tiny pricing function — everything from this lesson together ===
// TS signature:
// function price(base: number, taxRate: number = 0.2, ...fees: number[]): string
// - base: required number
// - taxRate: DEFAULT 0.2 (20%) if not given
// - fees: REST — zero or more extra charges
// - returns: a string
function price(base, taxRate = 0.2, ...fees) {
const tax = base * taxRate; // tax on the base
const extra = fees.reduce((t, f) => t + f,
...Order matters: the required base comes first, then the taxRate with its default, then the ...fees rest parameter — which must always be last.
Pro Tips
- 💡 Always annotate parameters. Return types can often be inferred, but typing parameters is what catches wrong calls.
- 💡 Prefer a default over an optional when there's a sensible fallback — it removes the
undefinedcheck entirely. - 💡 Put required params first. Optional, default, and rest parameters must come after the required ones.
- 💡 Name a function type with
type Handler = (e: Event) => void;and reuse it instead of repeating the signature.
Common Errors (and the fix)
- "TS7006: Parameter 'x' implicitly has an 'any' type": you forgot to type a parameter. Add an annotation —
function f(x: number)— or enable it knowingly. (This fires undernoImplicitAny, which is on instrictmode.) - "TS2554: Expected 2 arguments, but got 1" (or 3): you called the function with the wrong number of arguments. Make a parameter optional with
?or give it a default, or fix the call site. - Missing return type bites you: if you forget to
returnin a function annotated: number, TS reports "TS2355: A function whose declared type is neither 'void' nor 'any' must return a value". Add thereturn, or change the type tovoid. - "TS2322: Type 'string' is not assignable to type 'number'": you returned the wrong type. Return value must match the declared return type.
- "A required parameter cannot follow an optional parameter": reorder so required parameters come first.
📋 Quick Reference
| Feature | Syntax |
|---|---|
| Typed function | function f(a: number): string |
| Returns nothing | function log(m: string): void |
| Optional param | function f(name?: string) |
| Default param | function f(n: number = 0) |
| Rest param | function f(...xs: number[]) |
| Arrow function | const d = (n: number): number => n * 2 |
| Function type | type Op = (a: number, b: number) => number |
| Overload | function f(x: string): string; |
Frequently Asked Questions
Q: What's the difference between ? and = default?
Both let the caller skip the argument. With ? the parameter can be undefined, so you must handle that. With = value it falls back to that value automatically, so it's never undefined inside the function.
Q: Do I always have to write the return type?
No — TypeScript usually infers it from your return statements. But writing it is good practice: it documents intent and turns a wrong return into an error at the function, not at some far-away call site.
Q: When should I use overloads instead of a union type?
Use a union (string | number) when the return type is the same no matter the input. Use overloads when the relationship between input and output changes — e.g. a string input returns a string and a number input returns a number array.
Q: Is an arrow function the same as a regular function?
Almost — the big difference is this. Arrow functions don't bind their own this; they reuse the one from where they're defined, which makes them ideal for callbacks. For most plain helper functions, either style works.
Mini-Challenge: joinNames
No blanks this time — just a brief and an outline. Write the body yourself using a default parameter and a rest parameter, run it, and check your output against the expected lines.
🎯 Mini-Challenge: build joinNames
Combine a default separator and a rest parameter into one function.
// 🎯 MINI-CHALLENGE: a flexible "joinNames" function
// TS signature to aim for:
// function joinNames(separator: string = ", ", ...names: string[]): string
//
// 1. Give "separator" a DEFAULT of ", " (comma + space).
// 2. Use a REST parameter "...names" to accept any number of names.
// 3. Return the names glued together with the separator.
// (hint: an array has a .join(separator) method)
// 4. If no names are passed, return "(nobody)".
//
// ✅ Expected:
// joinNames(", ", "Ann", "Be
...🎉 Lesson Complete
- ✅ Annotate parameters and return types:
function f(a: number): string - ✅
voidmarks a function that returns nothing useful - ✅
?makes a parameter optional;= valuegives it a default - ✅
...args: T[]collects any number of arguments into a typed array - ✅ Arrow functions and function type expressions let you pass functions around safely
- ✅ Overloads expose several clean signatures over one implementation
- ✅ Next lesson: Classes and Objects — bundle data and behaviour together
Sign up for free to track which lessons you've completed and get learning reminders.