Skip to content

Functions

There are two main ways to define functions in TypeScript: function declarations and arrow functions. Both can be used interchangeably, but arrow functions have some differences in how they handle the this keyword.

A function declaration is a named function that can be called later in the code. It can take parameters and return a value.

function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, 3)); // 5

Using the function keyword, will “hoist” the function declaration to the top of the scope, meaning you can call it before it is defined in the code.

console.log(subtract(5, 2)); // 3
function subtract(a: number, b: number): number {
return a - b;
}

The return type of the function can be inferred and is often left off in application code:

function multiply(a: number, b: number) {
return a * b; // TypeScript infers the return type as number
}
let answer:number;
answer = multiply(4, 5); // answer is inferred as number

Arrow functions are a more concise way to write functions. They are often used for short, one-liner functions or when you want to preserve the this context from the surrounding scope.

const divide = (a: number, b: number): number => {
return a / b;
};
console.log(divide(10, 2)); // 5

Arrow functions can also be written in a more concise form if they have a single expression:

const square = (x: number): number => x * x;
console.log(square(4)); // 16

Arrow functions are not “hoisted” like function declarations, so you must define them before you use them:

console.log(modulus(10, 3)); // Error: Cannot access 'modulus' before initialization
const modulus = (a: number, b: number): number => {
return a % b;
};

Functions are visible in the scope in which they are defined. If you define a function inside another function, it is only visible within that inner function.

function outerFunction() {
function innerFunction() {
console.log("Hello from inner function");
}
innerFunction(); // This works
}
outerFunction();
// innerFunction(); // Error: innerFunction is not defined outside outerFunction

A function (either a function declaration or an arrow function) declared at the “root” of a source code file (called a “module”) is available throughout the module.

We will see below how to export functions, types, and values from a module and import them into another module.

You can define the type of a function using a function type annotation. This is useful when you want to pass functions as arguments or return them from other functions.

type MathOperation = (a: number, b: number) => number;
function performOperation(op: MathOperation, a: number, b: number): number {
return op(a, b);
}
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const result = performOperation(add, 5, 3);
const result2 = performOperation(subtract, 5, 3);
const result3 = performOperation((a, b) => a * b, 5, 3);
console.log({result, result2, result3})); // { result: 8, result2: 2, result3: 15 }

You can make parameters optional by adding a ? after the parameter name. Optional parameters must come after all required parameters.

function greet(name: string, greeting?: string) {
return `${greeting || "Hello"}, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob", "Hi")); // "Hi, Bob!"

Note that unlike some languages, TypeScript does not support “named parameters” - you must pass parameters in the order they are defined. So default parameters are listed last, and are used if the caller does not provide a value.

it('example 2 - named parameters bad', () => {
function greet(name: string, greeting = 'Hello', upper = false) {
const greetMessage = `${greeting}, ${name}!`;
return upper ? greetMessage.toUpperCase() : greetMessage;
}
expect(greet('Alice')).toBe('Hello, Alice!');
expect(greet('Bob', 'Hi')).toBe('Hi, Bob!');
expect(greet('Charlie', 'Hey', true)).toBe('HEY, CHARLIE!');
// expect(greet('Dave', upper: true)).toBe('HELLO, DAVE!');
});

Optional Parameters can be indicated with the ? (sus) suffix, but should be listed after all required parameters.

it('optional parameters', () => {
function buildName(firstName: string, lastName?: string) {
return lastName ? `${firstName} ${lastName}` : firstName;
}
expect(buildName('John', 'Doe')).toBe('John Doe');
expect(buildName('Jane')).toBe('Jane');
});

A common pattern is to make monoidal functions that take a single argument object. This borders of utility folder functions, but it is handy to know:

it('destructured parameters', () => {
function createUser({ name, age }: { name: string; age: number }) {
return { name: name.toUpperCase(), age: age + 1 };
}
const { name, age } = createUser({ name: 'Alice', age: 30 });
expect(name).toBe('ALICE');
expect(age).toBe(31);
});

The final argument of a function can be an array of values, using the ... syntax. This is called a “rest parameter”.

it('rest parameters', () => {
function sum(start = 0, ...numbers: number[]) {
return numbers.reduce((acc, curr) => acc + curr, start);
}
expect(sum(1, 2, 3)).toBe(6);
expect(sum(10, 20, 30, 40)).toBe(100);
expect(sum()).toBe(0);
expect(sum(5, 2, 3)).toBe(10);
});

Higher order functions are functions that take other functions as arguments or return functions as their result.

it('higher-order functions', () => {
function sum(predicate: (n: number) => boolean, ...numbers: number[]) {
return numbers.filter(predicate).reduce((acc, curr) => acc + curr, 0);
}
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
expect(sum((n) => n % 2 === 0, ...numbers)).toBe(30);
const isOdd = (n: number) => n % 2 !== 0;
expect(sum(isOdd, ...numbers)).toBe(25);
});

A more advanced example is a function that returns a function:

it('function returning a function', () => {
function makeMultiplier(factor: number): (n: number) => number {
return (n: number) => n * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
expect(double(5)).toBe(10);
expect(triple(5)).toBe(15);
});

Or, maybe more practical, but introducing some testing mocks:

it('higher-order functions 2', () => {
function logWithPrefix(prefix: string) {
return function (message: string) {
console.log(`${prefix}: ${message}`);
};
}
const consoleMock = vi
.spyOn(console, 'log')
.mockImplementation(() => undefined);
const infoLogger = logWithPrefix('INFO');
infoLogger('This is an informational message.');
expect(consoleMock).toHaveBeenCalledWith(
'INFO: This is an informational message.',
);
const featureLogger = logWithPrefix('Feature 1');
featureLogger('Feature 1 is working!');
expect(consoleMock).toHaveBeenCalledWith(
'Feature 1: Feature 1 is working!',
);
consoleMock.mockRestore();
});

Overloading means having one function that can accept a variety of different inputs. Much of the usefulness of this can be accomplished by using union types, discriminated unions, and destructured parameters, however, it is used a lot in libraries, and does provide good editor feedback.

it('function overloads', () => {
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
function combine(a: unknown, b: unknown): unknown {
if (typeof a === 'string' && typeof b === 'string') {
return a + b;
}
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
throw new Error('Invalid arguments');
}
const combinedStrings = combine('Hello, ', 'World!');
expect(combinedStrings).toBe('Hello, World!');
const combinedNumbers = combine(10, 20);
expect(combinedNumbers).toBe(30);
// @ts-expect-error can't combine string and number
combine('dog', 5);
});

Modules: Exporting and Importing Functions

Section titled “Modules: Exporting and Importing Functions”

In TypeScript, each file is treated as a separate module. You can export functions, types, and values from a module and import them into another module.

Modules (just TypeScript files) can export multiple functions, types, and values. You can use the export keyword to make a function available outside the module.

They can also export a single “default” function, type, or value using the export default syntax.

export const add = (a: number, b: number): number => a + b;
export default (a: number, b: number): number => a - b;