Skip to lesson

learningtypescript.org / basics / 03-functions · lesson 3 of 25

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, = value for 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?

Continue learning

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");
Continue learning

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:

  1. Create a function that takes two strings and returns them concatenated
  2. Write an arrow function that calculates the area of a circle
  3. Create a function with an optional parameter for a discount percentage
  4. Write a function using rest parameters to find the maximum number
  5. 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

Continue learning

Once it passes, try two variations and predict each before running:

  1. Drop the default. Rewrite describe's signature as function describe(value: number, tag: string): string | undefined — no = "shape" default. Predict what describe(15) returns with no second argument before running it. The runner strips types and does not stop you, so tag arrives as undefined, the string becomes "undefined: 15", and the default-tag check fails — the exact reason the default parameter was there.
  2. The arrow that forgets to return. Give perimeter a block body with no return: change return 2 * (width + height); to just 2 * (width + height);. Decide what perimeter(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 returns undefined and 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.