TL;DR
Learn how to write typed functions in TypeScript with optional parameters, overloads, and return types for safer, documented code.
Key concepts
- TypeScript functions
- typed functions
- function overloads TypeScript
- arrow functions TypeScript
Functions
TL;DR — Annotate function parameters and return types to lock down the contract:
function add(a: number, b: number): number. Use?for optional params,= valuefor defaults,...args: T[]for rest params. Arrow functions share the same annotation syntax. TypeScript infers return types, but explicit annotations improve readability and IDE tooltips.
Functions are the building blocks of readable, maintainable code. In TypeScript, functions can be typed for parameters and return values, providing better safety and documentation.
Basic Function Syntax
Here's how you define a simple function in TypeScript:
function greet(): void {
console.log("Hello, TypeScript!");
}
greet();
Functions with Parameters
Functions can take typed parameters:
function greetPerson(name: string): void {
console.log(`Hello, ${name}!`);
}
greetPerson("Alice");
Return Types
Functions can return values with explicit type annotations:
function add(x: number, y: number): number {
return x + y;
}
const result = add(5, 3);
console.log(`5 + 3 = ${result}`);
Optional Parameters
Parameters can be made optional using the ? operator:
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return `${firstName} ${lastName}`;
}
return firstName;
}
console.log(buildName("Alice"));
console.log(buildName("Bob", "Smith"));
Default Parameters
You can provide default values for parameters:
function greetWithDefault(name: string = "Guest"): void {
console.log(`Hello, ${name}!`);
}
greetWithDefault();
greetWithDefault("Alice");
Recall
Reaching back to 02-variables-and-types, no scrolling: when an optional parameter like lastName?: string is left out at the call site (buildName('Alice')), what value does it hold inside the function?
Arrow Functions
Arrow functions provide a concise syntax for writing functions:
// Regular function
function multiply(a: number, b: number): number {
return a * b;
}
// Arrow function
const multiplyArrow = (a: number, b: number): number => {
return a * b;
};
// Concise arrow function (implicit return)
const multiplyShort = (a: number, b: number): number => a * b;
console.log(`multiply(5, 3) = ${multiply(5, 3)}`);
console.log(`multiplyArrow(5, 3) = ${multiplyArrow(5, 3)}`);
console.log(`multiplyShort(5, 3) = ${multiplyShort(5, 3)}`);
Function Types
You can define types for functions:
// Function type
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
console.log(`10 + 5 = ${add(10, 5)}`);
console.log(`10 - 5 = ${subtract(10, 5)}`);
Rest Parameters
Functions can accept a variable number of arguments:
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(`sum(1, 2, 3) = ${sum(1, 2, 3)}`);
console.log(`sum(1, 2, 3, 4, 5) = ${sum(1, 2, 3, 4, 5)}`);
Generic Functions
Generics allow you to write reusable functions that work with different types. This is just an intro — the Generics lesson covers constraints, conditional types, and real-world patterns in depth.
// Generic function
function identity<T>(arg: T): T {
return arg;
}
const numberResult = identity<number>(42);
const stringResult = identity<string>("Hello");
console.log(`Number identity: ${numberResult}`);
console.log(`String identity: ${stringResult}`);
// TypeScript can infer the generic type
const inferredNumber = identity(42);
const inferredString = identity("Hello");
console.log(`Inferred number: ${inferredNumber}`);
console.log(`Inferred string: ${inferredString}`);
Predict
Here identity is called with no <...> type argument written. TypeScript infers T from the value you pass. What type does the compiler infer for result?
function identity<T>(arg: T): T {
return arg;
}
let result = identity("Hello");Try It Yourself
Practice writing functions in the playground below:
// Basic function
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greet("TypeScript"));
// Function with multiple parameters
function calculateRectangleArea(width: number, height: number): number {
return width * height;
}
console.log(`Area of 5x3 rectangle: ${calculateRectangleArea(5, 3)}`);
// Arrow function
const double = (n: number): number => n * 2;
console.log(`double(21) = ${double(21)}`);
// Function with optional parameter
function createGreeting(name: string, title?: string): string {
if (title) {
return `Hello, ${title} ${name}!`;
}
return `Hello, ${name}!`;
}
console.log(createGreeting("Alice"));
console.log(createGreeting("Smith", "Dr."));
// Function with default parameter
function power(base: number, exponent: number = 2): number {
return Math.pow(base, exponent);
}
console.log(`3^2 = ${power(3)}`);
console.log(`2^3 = ${power(2, 3)}`);
// Rest parameters
function average(...numbers: number[]): number {
const sum = numbers.reduce((total, n) => total + n, 0);
return sum / numbers.length;
}
console.log(`Average of 10, 20, 30: ${average(10, 20, 30)}`);
Try these exercises:
- Create a function that takes two strings and returns them concatenated
- Write an arrow function that calculates the area of a circle
- Create a function with an optional parameter for a discount percentage
- Write a function using rest parameters to find the maximum number
- Create a generic function that returns the first element of an array
Reading about functions is not the same as writing them. This is a build task: a small program that reports its own pass/fail. You are given fixed rectangle data and three empty functions to finish — a declaration, an arrow function, and one with a default parameter, each fully typed. Run it and it fails immediately, telling you which function is still missing. Implement each one until every check passes and it prints All checks passed.
The three functions reuse exactly what this lesson taught: writing a typed declaration and a typed arrow function, taking parameters, returning a value with return, and giving a parameter a default. The starter already has the data, the stubs, and the checks — you write only the body of each function. Nothing above spells out all three answers, so you will have to assemble them yourself.
Build
Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide what that function is missing, then implement the three functions until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.
import assert from "node:assert";
// The data: rectangle dimensions to measure. Do NOT change this array.
const rectangles: { label: string; width: number; height: number }[] = [
{ label: "small", width: 5, height: 3 },
{ label: "wide", width: 10, height: 4 },
{ label: "square", width: 6, height: 6 },
];
// TODO 1: return the area of a rectangle (width times height).
// area(5, 3) -> 15
function area(width: number, height: number): number | undefined {
return undefined; // replace undefined with your code
}
// TODO 2: return the perimeter of a rectangle: 2 * (width + height).
// perimeter(5, 3) -> 16
const perimeter = (width: number, height: number): number | undefined => {
return undefined; // replace undefined with your code
};
// TODO 3: return a label like "small: 15" — the tag, a colon, a space, then the value.
// Give tag a default of "shape", so describe(15) uses "shape".
// describe(15, "small") -> "small: 15"
// describe(15) -> "shape: 15"
function describe(value: number, tag: string = "shape"): string | undefined {
return undefined; // replace undefined with your code
}
// --- Build checks: these must all pass. Do not edit below this line. ---
assert.strictEqual(
area(5, 3),
15,
"area(width, height) should return width times height",
);
assert.strictEqual(
perimeter(5, 3),
16,
"perimeter(width, height) should return 2 * (width + height)",
);
assert.strictEqual(
describe(15),
"shape: 15",
"describe(value) should default its tag to 'shape' when no tag is passed",
);
assert.strictEqual(
describe(15, "small"),
"small: 15",
"describe(value, tag) should join the tag and value as 'tag: value'",
);
console.log("All checks passed.");
console.log("Area 5x3:", area(5, 3));
console.log("Perimeter 5x3:", perimeter(5, 3));
console.log("Described:", describe(15, rectangles[0].label));Expected output: All checks passed.
Area 5x3: 15
Perimeter 5x3: 16
Described: small: 15
Once it passes, try two variations and predict each before running:
- Drop the default. Rewrite
describe's signature asfunction describe(value: number, tag: string): string | undefined— no= "shape"default. Predict whatdescribe(15)returns with no second argument before running it. The runner strips types and does not stop you, sotagarrives asundefined, the string becomes"undefined: 15", and the default-tag check fails — the exact reason the default parameter was there. - The arrow that forgets to return. Give
perimetera block body with noreturn: changereturn 2 * (width + height);to just2 * (width + height);. Decide whatperimeter(5, 3)yields before running. A{ }arrow body does not return its last expression the way a concise=> 2 * (width + height)body does, so the function returnsundefinedand the perimeter check fails.
Key Takeaways
- Functions can have typed parameters and return types
- Use
?for optional parameters and=for default values - Arrow functions provide concise syntax
- Rest parameters allow variable-length argument lists
- Generics enable reusable, type-safe functions
- TypeScript infers return types, but explicit types improve clarity
Pro Tip: Start by writing your functions with explicit type annotations. As you become more comfortable, you can rely on TypeScript's type inference for simpler cases!
Next Steps
With typed functions under your belt, you're ready to control how your program flows. Next up: conditionals, loops, switch statements, and how TypeScript narrows types inside control flow branches.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.