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.
Function Declarations
Section titled “Function Declarations”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)); // 5Using 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)); // 3function 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 numberArrow Functions
Section titled “Arrow Functions”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)); // 5Arrow 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)); // 16Arrow 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 initializationconst modulus = (a: number, b: number): number => { return a % b;};Function Scope
Section titled “Function Scope”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 outerFunctionA 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.
Function Types
Section titled “Function Types”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 }Arguments to Functions
Section titled “Arguments to Functions”Optional and Default Parameters
Section titled “Optional and Default Parameters”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
Section titled “Optional Parameters”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'); });Destructured Parameters
Section titled “Destructured Parameters”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); });Rest Parameters
Section titled “Rest Parameters”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
Section titled “Higher Order Functions”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(); });Function Overloading
Section titled “Function Overloading”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;import { describe, expect, it } from 'vitest';import subby, { add } from './utils';describe('feature-two/utils', () => { it('should be able to use the add function', () => { expect(add(2, 3)).toBe(5); }); it('should be able to use the subtract function', () => { expect(subby(5, 2)).toBe(3); });});