Skip to main content

Function basics

In this chapter, you learn the basics of writing functions in TypeScript.

What you learn in this chapter

  • How to write type annotations for functions
  • The syntax and usage of arrow functions
  • The basics of function expressions
  • Default parameters and optional parameters
  • void-type functions
info

You can try the code examples in this chapter on the TypeScript Playground.

Type annotations for functions

In TypeScript, you can specify types for a function's parameters and return value. This makes the function's usage clear and catches mistakes at the call site at compile time.

Basic syntax

// The basic syntax of a function type annotation
function functionName(parameterName: parameterType): returnType {
// processing
return returnValue;
}

A concrete example

// A function that adds two numbers
function add(a: number, b: number): number {
return a + b;
}

// Usage examples
console.log(add(5, 3)); // 8
console.log(add(10, 20)); // 30

// An example of a type error (an error at compile time)
// console.log(add('5', '3'));
// Error: Argument of type 'string' is not assignable to parameter of type 'number'

Code explanation:

  • a: number - the first parameter a is of type number
  • b: number - the second parameter b is of type number
  • : number (after the function name) - the return type is number
  • A call with mismatched types is a compile error

Inference of the return type

TypeScript can infer the return type automatically.

// Specify the return type explicitly
function add1(a: number, b: number): number {
return a + b;
}

// Omit the return type (leave it to inference)
function add2(a: number, b: number) {
return a + b; // the return type is inferred as number
}

// Both work the same way
console.log(add1(5, 3)); // 8
console.log(add2(5, 3)); // 8

Best practices:

// Recommended: always add type annotations to parameters
function multiply(a: number, b: number) {
return a * b;
}

// For complex functions, also state the return type explicitly (improves readability)
function complexCalculation(x: number, y: number): number {
const step1 = x * 2;
const step2 = y + 10;
const result = step1 + step2;
return result;
}

// Not recommended: parameters become the any type without type annotations
function bad(a, b) { // a and b are of type any
return a + b;
}

void-type functions

A function with no return value returns the void type.

// A function with no return value (it just prints to the console)
function greet(name: string): void {
console.log(`Hello, ${name}!`);
// no return statement, or just return;
}

greet('Alice'); // 'Hello, Alice!'

// You cannot use the return value of a function that returns void
const result = greet('Bob');
console.log(result); // undefined

Practical examples of void:

// A function that saves data (no return value needed)
function saveData(data: string): void {
localStorage.setItem('userData', data);
console.log('Data saved successfully');
}

// A function that prints a log
function log(level: string, message: string): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${message}`);
}

log('INFO', 'Application started');
log('ERROR', 'Something went wrong');

Arrow functions

A concise way to write functions, introduced in ES2015. Modern TypeScript code uses them frequently.

Basic syntax

// A traditional function expression
const add1 = function(a: number, b: number): number {
return a + b;
};

// An arrow function (same feature)
const add2 = (a: number, b: number): number => {
return a + b;
};

// An arrow function (short form: when there is a single expression)
const add3 = (a: number, b: number): number => a + b;

// All produce the same result
console.log(add1(5, 3)); // 8
console.log(add2(5, 3)); // 8
console.log(add3(5, 3)); // 8

The short form of arrow functions

// The normal way
const square1 = (x: number): number => {
return x * x;
};

// Short form: omit the braces and return (when there is a single expression)
const square2 = (x: number): number => x * x;

// Parentheses are required when there is a type annotation
const square3 = (x: number) => x * x;

// Parentheses are required when there are zero parameters
const getRandom = (): number => Math.random();

console.log(square1(5)); // 25
console.log(square2(5)); // 25
console.log(square3(5)); // 25
console.log(getRandom()); // 0.123... (a random value)

When returning an object

// When returning an object, wrap it in parentheses
const createUser = (name: string, age: number) => ({
name: name,
age: age
});

// Or the normal way with braces
const createUser2 = (name: string, age: number) => {
return {
name: name,
age: age
};
};

console.log(createUser('Alice', 25)); // { name: 'Alice', age: 25 }
console.log(createUser2('Bob', 30)); // { name: 'Bob', age: 30 }
warning

When returning an object literal directly, you must wrap it in parentheses; otherwise the braces are interpreted as the function block and it errors.

Combining with array methods

Arrow functions are often used with array methods.

const numbers = [1, 2, 3, 4, 5];

// map: double each element
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter: extract only the even numbers
const evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4]

// reduce: calculate the sum
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

// find: get the first element that matches a condition
const found = numbers.find((n) => n > 3);
console.log(found); // 4

Function expressions

A way to assign a function to a variable.

A basic function expression

// The basic syntax of a function expression
const add = function(a: number, b: number): number {
return a + b;
};

console.log(add(5, 3)); // 8

A type annotation for a function

You can also state a function's type explicitly on the variable.

// A type annotation on the variable (states the function's type)
const multiply: (a: number, b: number) => number = function(a, b) {
return a * b;
};

console.log(multiply(4, 5)); // 20

A variable of a function type

// Define a function's type
// Syntax: (parameter: type, ...) => returnType
let operation: (a: number, b: number) => number;

// You can assign a function that matches this type
operation = function(a, b) {
return a + b;
};
console.log(operation(10, 20)); // 30

// Assign a different function
operation = function(a, b) {
return a * b;
};
console.log(operation(10, 20)); // 200

Default parameters

You can set a default value for a parameter. When the argument is omitted, the default value is used.

// The basics of default parameters
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}

console.log(greet('Alice')); // 'Hello, Alice!'
console.log(greet('Bob', 'Hi')); // 'Hi, Bob!'
console.log(greet('Charlie', 'Hey')); // 'Hey, Charlie!'

A practical example

function createUser(
name: string,
age: number = 0,
isActive: boolean = true
) {
return { name, age, isActive };
}

console.log(createUser('Alice'));
// { name: 'Alice', age: 0, isActive: true }

console.log(createUser('Bob', 25));
// { name: 'Bob', age: 25, isActive: true }

console.log(createUser('Charlie', 30, false));
// { name: 'Charlie', age: 30, isActive: false }

Default parameters and type inference

// The type is inferred from the default value
function multiply(a: number, b = 2) { // the type of b is inferred as number
return a * b;
}

console.log(multiply(5)); // 10 (5 * 2)
console.log(multiply(5, 3)); // 15 (5 * 3)

Optional parameters

Adding ? after a parameter name makes that parameter optional.

// Basic usage
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}

console.log(greet('Alice')); // 'Hello, Alice!'
console.log(greet('Bob', 'Good morning')); // 'Good morning, Bob!'

A note on optional parameters

// Place optional parameters after the required ones
// Correct: the optional parameters come last
function correct(a: number, b?: number, c?: number) {
console.log(a, b, c);
}

correct(1); // 1 undefined undefined
correct(1, 2); // 1 2 undefined
correct(1, 2, 3); // 1 2 3

// Wrong: a required parameter comes after an optional one
// function wrong(a?: number, b: number) {} // Error

The type of an optional parameter

The type of an optional parameter automatically has | undefined added.

function printValue(value?: string): void {
// the type of value is string | undefined
console.log(value);

// an existence check is needed before use
// console.log(value.toUpperCase()); // Error: 'value' is possibly 'undefined'

// the correct usage
if (value) {
console.log(value.toUpperCase());
}

// or optional chaining
console.log(value?.toUpperCase());
}

printValue(); // undefined, undefined
printValue('hello'); // hello, HELLO, HELLO

Default parameters vs. optional parameters

CharacteristicDefault parameterOptional parameter
Syntaxparam = valueparam?
Value when omittedthe specified default valueundefined
undefined checknot neededneeded
Use caseusually use a specific valuea value may or may not be present
// Default parameter: use 'Hello' when omitted
function greet1(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`; // greeting is always a string
}

// Optional parameter: greeting may be undefined
function greet2(name: string, greeting?: string): string {
if (greeting) { // an undefined check is needed
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}

Try it: build a calculation utility ★

Create a set of calculation utility functions that meet the following requirements.

Requirements:

  1. add(a, b): a function that adds two numbers
  2. subtract(a, b): a function that subtracts two numbers
  3. multiply(a, b): a function that multiplies two numbers (the default value of b is 1)
  4. divide(a, b): a function that divides two numbers (returns null when b is 0)
  5. calculate(a, b, operation): a function that takes two numbers and a kind of operation ('add', 'subtract', 'multiply', 'divide') and returns the result
Hint
  1. Add type annotations to the parameters and return value of every function
  2. Use a default parameter in multiply
  3. In divide, check for division by zero, and make the return type number | null
  4. In calculate, branch on the value of operation
Answer and explanation
// A function that adds two numbers
const add = (a: number, b: number): number => a + b;

// A function that subtracts two numbers
const subtract = (a: number, b: number): number => a - b;

// A function that multiplies two numbers (the default value of b is 1)
const multiply = (a: number, b: number = 1): number => a * b;

// A function that divides two numbers (returns null when b is 0)
const divide = (a: number, b: number): number | null => {
if (b === 0) {
return null; // prevent division by zero
}
return a / b;
};

// A function that takes two numbers and a kind of operation and returns the result
const calculate = (
a: number,
b: number,
operation: 'add' | 'subtract' | 'multiply' | 'divide'
): number | null => {
switch (operation) {
case 'add':
return add(a, b);
case 'subtract':
return subtract(a, b);
case 'multiply':
return multiply(a, b);
case 'divide':
return divide(a, b);
default:
return null;
}
};

// Tests
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(multiply(5, 3)); // 15
console.log(multiply(5)); // 5 (default value 1)
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // null
console.log(calculate(10, 5, 'add')); // 15
console.log(calculate(10, 5, 'subtract')); // 5
console.log(calculate(10, 5, 'multiply')); // 50
console.log(calculate(10, 5, 'divide')); // 2
console.log(calculate(10, 0, 'divide')); // null

Explanation:

  • add, subtract: basic functions that use the short form of arrow functions
  • multiply: uses the default parameter b = 1 to make the second argument optional
  • divide: makes the return type number | null to guard against division by zero
  • calculate: uses the literal-type union 'add' | 'subtract' | 'multiply' | 'divide' to accept only valid operations

Summary

What you learned in this chapter:

  • Type annotations for functions: specifying types for parameters and the return value lets you define type-safe functions
  • Arrow functions: a concise way to write functions with =>, which pairs well with array methods
  • Function expressions: assign a function to a variable and state the function's type explicitly
  • Default parameters: set the default value used when omitted with param = value
  • Optional parameters: define parameters that can be omitted with param?
  • The void type: represents a function with no return value

In the next chapter, you will learn advanced function topics such as function overloads, callback functions, and rest parameters.

Common pitfalls for beginners

warning

Common mistakes

❌ Forgetting the parentheses when returning an object from an arrow function

// ❌ Wrong: the braces are interpreted as the function block
const createUser = (name: string) => { name: name };
// This does not return { name: name };
// it is interpreted as an expression statement with a label called name:

// ✅ Correct: wrap it in parentheses to return it as an object literal
const createUser = (name: string) => ({ name: name });

// Or the normal way
const createUser2 = (name: string) => {
return { name: name };
};

Cause: In JavaScript, {} is used for both function blocks and object literals.

Solution: When you return an object directly, wrap it in ({}).

❌ Confusing default parameters with optional parameters

// ❌ Wrong: adding optional (?) when there is already a default value
function greet(name?: string = 'Guest'): void { // Error
console.log(`Hello, ${name}!`);
}

// ✅ Correct: an optional marker is unnecessary when there is a default value
function greet(name: string = 'Guest'): void {
console.log(`Hello, ${name}!`); // name is always a string
}

// When you use an optional parameter (it may be undefined)
function greet2(name?: string): void {
console.log(`Hello, ${name ?? 'Guest'}!`); // name is string | undefined
}

Cause: A default parameter automatically becomes optional, so ? is redundant.

Solution: When you set a default value, do not add ?; use only = value.

❌ Placing an optional parameter before a required one

// ❌ Wrong: the optional parameter comes first
function wrong(a?: number, b: number): number {
// Error: A required parameter cannot follow an optional parameter
return (a ?? 0) + b;
}

// ✅ Correct: place the required parameter first
function correct(b: number, a?: number): number {
return (a ?? 0) + b;
}

Cause: Because JavaScript function calls use positional arguments, optional parameters must come last.

Solution: Always place optional parameters after the required ones.

❌ Forgetting the return type annotation, so it becomes any

// ❌ Wrong: when noImplicitAny is disabled, the parameters become the any type
function add(a, b) { // a and b are of type any
return a + b;
}

add('5', '10'); // '510' (string concatenation, unintended behavior)

// ✅ Correct: add type annotations to the parameters
function add(a: number, b: number): number {
return a + b;
}

// add('5', '10'); // Error: caught as a type error

Cause: When TypeScript's noImplicitAny option is disabled, parameters without type annotations become any.

Solution: Set noImplicitAny: true in tsconfig.json, and always add type annotations.

❌ Trying to use the return value of a void function

function logMessage(msg: string): void {
console.log(msg);
}

// ❌ Wrong: trying to use the return value of a void function
const result = logMessage('Hello');
if (result) { // result is void (undefined)
console.log('Success'); // never runs
}

// ✅ Correct: expect a void function to only have side effects
logMessage('Hello');

// When you need a return value, define the appropriate type
function formatMessage(msg: string): string {
return `[INFO] ${msg}`;
}
const formatted = formatMessage('Hello');
console.log(formatted); // '[INFO] Hello'

Cause: A void-type function returns no value, so its return value is undefined.

Solution: When you need a return value, define the appropriate return type.