Functions and Generics
Reviewed & published by Brayan K
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.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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 void for 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
💡 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.
// 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') =", shout("hi")); // HI!
// A function that does NOT return a value has return type "void".
// TS signature: function log(message: string): void
function log(message) {
console.log("[LOG]", message); // prints, returns nothing
}
log("server started"); // [LOG] server started2. 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 — 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 + ", " + name + "!"
return ___; // 👉 e.g. greeting + ", " + name + "!"
}
console.log(greet("Alice")); // uses the default greeting
console.log(greet("Bob", "Hey")); // overrides the default
// ✅ Expected output:
// Hello, Alice!
// Hey, Bob!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.
// 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: number): number => n * 2
const double = (n) => n * 2;
console.log("double(8) =", double(8)); // 16
// FUNCTION TYPE EXPRESSION — describe a function's SHAPE as a type.
// TS: type MathOp = (a: number, b: number) => number
// const multiply: MathOp = (a, b) => a * b;
// The type says: "takes two numbers, returns a number."
const multiply = (a, b) => a * b;
const subtract = (a, b) => a - b;
// Because they share a shape, you can pass one function to another:
function applyOp(a, b, op) { // op: MathOp
return op(a, b);
}
console.log("applyOp(6, 4, multiply) =", applyOp(6, 4, multiply)); // 24
console.log("applyOp(6, 4, subtract) =", applyOp(6, 4, subtract)); // 2Your turn again — finish the average function. It uses a rest parameter and an arrow inside reduce. Fill in the two blanks:
// 🎯 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.log("average(10, 20, 30) =", average(10, 20, 30));
console.log("average(4, 8) =", average(4, 8));
console.log("average() =", average());
// ✅ Expected output:
// average(10, 20, 30) = 20
// average(4, 8) = 6
// average() = 04. 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.
// 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
}
return value.toFixed(2); // number branch
}
console.log("format(' hi ') =", format(" hi ")); // HI
console.log("format(3.14159) =", format(3.14159)); // 3.14
// Overloads let TS know that format(string) AND format(number)
// both safely return a string — with no "any" in sight.🔎 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.
// === 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, 0); // sum of all fees
const total = base + tax + extra;
return "Total: £" + total.toFixed(2); // return type: string
}
console.log(price(100)); // base only, default 20% tax
console.log(price(100, 0.1)); // custom 10% tax
console.log(price(100, 0.1, 5, 2)); // 10% tax + two fees (5 and 2)
// ✅ Output:
// Total: £120.00
// Total: £110.00
// Total: £117.00Order 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 undefined check 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 under noImplicitAny, which is on in strict mode.)
- "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 return in a function annotated : number, TS reports "TS2355: A function whose declared type is neither 'void' nor 'any' must return a value". Add the return, or change the type to void.
- "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: 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", "Ben", "Cara") → Ann, Ben, Cara
// joinNames(" & ", "Sam", "Lee") → Sam & Lee
// joinNames() → (nobody)
// your code here
function joinNames(separator, ...names) {
}
console.log(joinNames(", ", "Ann", "Ben", "Cara"));
console.log(joinNames(" & ", "Sam", "Lee"));
console.log(joinNames());🎉 Lesson Complete
- ✅ Annotate parameters and return types: function f(a: number): string
- ✅ void marks a function that returns nothing useful
- ✅ ? makes a parameter optional; = value gives 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
Practice quiz
What is the shape of a typed function declaration?
- function name(param): Type { }
- function name<Type>(param) { }
- function name(param: Type): ReturnType { }
- function name: ReturnType(param) { }
Answer: function name(param: Type): ReturnType { }. Annotate each parameter (param: Type) and the return type after the parentheses.
What return type marks a function that returns nothing useful?
- void
- null
- undefined
- never
Answer: void. void means the function is called for its effect, not its result.
What is the difference between '?' and '= default' on a parameter?
- No difference
- '?' sets a value; '= value' makes it optional
- Only '?' works in strict mode
- '?' may be undefined (you must handle it); '= value' falls back automatically
Answer: '?' may be undefined (you must handle it); '= value' falls back automatically. Optional parameters can be undefined; default parameters supply a value so they are never undefined.
What does a rest parameter '...numbers: number[]' do?
- Accepts exactly one array
- Collects any number of trailing arguments into a typed array
- Spreads an array into arguments
- Marks the parameter optional
Answer: Collects any number of trailing arguments into a typed array. A rest parameter gathers the remaining arguments into an array.
Where must optional, default, and rest parameters appear?
- After the required ones
- Before required ones
- Anywhere
- Only alone
Answer: After the required ones. Required parameters come first; optional/default/rest must follow them (rest always last).
Which error fires when you forget to type a parameter under strict mode?
- TS2322
- TS2554
- TS7006: Parameter implicitly has an 'any' type
- TS2355
Answer: TS7006: Parameter implicitly has an 'any' type. noImplicitAny (on in strict mode) raises TS7006 for an untyped parameter.
An arrow function with braces and multiple statements must:
- Omit the return
- Write 'return' explicitly
- Use the function keyword
- Be a method
Answer: Write 'return' explicitly. Implicit return only works for a single expression; with braces you must write return.
Compared with a regular function, an arrow function's key difference is:
- It is faster
- It cannot take arguments
- It always returns void
- It does not bind its own 'this' — it captures the surrounding one
Answer: It does not bind its own 'this' — it captures the surrounding one. Arrow functions reuse the enclosing this, which makes them ideal for callbacks.
What do function overloads let one function do?
- Run twice
- Advertise several typed call signatures over one implementation
- Skip type checking
- Return any type
Answer: Advertise several typed call signatures over one implementation. You write several signatures, then one implementation; callers see only the clean signatures.
When should you use a union type instead of overloads?
- Always
- When the input is an array
- When the return type is the same regardless of the input
- Never
Answer: When the return type is the same regardless of the input. A union like string | number is enough when the return type does not vary by input.
Continue this course
- Previous: Interfaces and Type Aliases
- Next: Classes and Objects — Access modifiers, readonly, abstract classes, and OOP in TypeScript
- Quick reference: TypeScript cheat sheet › Functions & Generics