Functions and Type Annotations

Functions are a core part of both JavaScript and TypeScript. Type annotations can be used with function parameters and return types to add type safety.

Types with function parameters

We have briefly covered using types with function parameters, the syntax looks like this:

function createPizza(name: string): string {
  return `Created pizza: ${name}`;
}

The name parameter is typed as string, and the function returns a string.

Function return types

You can specify what type a function returns:

function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

The : number after the parentheses (()) means this function returns a type of number.

Multiple parameters

Functions can have multiple parameters, each with it's own type:

function createPizzaOrder(pizzaName: string, quantity: number, price: number): string {
  return `Order: ${quantity}x ${pizzaName} - Total: $${(quantity * price).toFixed(2)}`;
}

Optional parameters

You can make parameters optional using the ? operator. This example requires a pizza to be passed to the function, but has optional toppings:

function addTopping(pizza: string, topping?: string): string {
  if (topping) {
    return `${pizza} with ${topping}`;
  }
  return pizza;
}

console.log(addTopping("Margherita")); // Margherita
console.log(addTopping("Margherita", "pepperoni")); // Margherita with pepperoni

Loading code...

function addTopping(pizza: string, topping?: string): string {
  if (topping) {
    return `${pizza} with ${topping}`;
  }
  return pizza;
}

console.log(addTopping("Margherita")); // Margherita
console.log(addTopping("Margherita", "pepperoni")); // Margherita with pepperoni

Default parameters

Parameters can have default values provided. These values are used if no value if provided. This example sets the pizza size to be medium unless an overriding value is passed:

function createPizza(name: string, size: string = "medium"): string {
  return `${size} ${name}`;
}

console.log(createPizza("Margherita")); // "medium Margherita"
console.log(createPizza("Margherita", "large")); // "large Margherita"

Loading code...

function createPizza(name: string, size: string = "medium"): string {
  return `${size} ${name}`;
}

console.log(createPizza("Margherita")); // "medium Margherita"
console.log(createPizza("Margherita", "large")); // "large Margherita"

TypeScript can infer the type from the default value, but it's good practice to be explicit:

function calculateTotal(price: number, tax: number = 0.1): number {
  return price * (1 + tax);
}

Rest parameters

You can use rest parameters to accept a variable number of arguments:

function addToppings(...toppings: string[]): string {
  return toppings.join(", ");
}

console.log(addToppings("pepperoni", "peppers"));
console.log(addToppings("pepperoni", "mushrooms", "olives", "peppers"));

Loading code...

function addToppings(...toppings: string[]): string {
  return toppings.join(", ");
}

console.log(addToppings("pepperoni", "peppers"));
console.log(addToppings("pepperoni", "mushrooms", "olives", "peppers"));

Arrow functions

Arrow functions use type annotations in the same way:

const greet = (name: string): string => {
  return `Hello, ${name}!`;
};

const add = (a: number, b: number): number => a + b;
console.log(greet('Maggie'));
console.log(add(5, 10));

Loading code...

const greet = (name: string): string => {
  return `Hello, ${name}!`;
};

const add = (a: number, b: number): number => a + b;
console.log(greet('Maggie'));
console.log(add(5, 10));

Function overloads

TypeScript supports function overloads (multiple function signatures):

function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value * 2;
}

console.log(process("typescript")); // TYPESCRIPT
console.log(process(7)); // 14
console.log(process(7, "typescript")); // Expected 1 arguments, but got 2.

Loading code...

function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value * 2;
}

console.log(process("typescript")); // TYPESCRIPT
console.log(process(7)); // 14
console.log(process(7, "typescript")); // Expected 1 arguments, but got 2.

The error "Expected 1 arguments, but got 2." is beause all functions are set to receive 1 argument.

Functions as parameters

Functions that are passed as parameters can also be typed:

function calculate(a: number, b: number, operation: (x: number, y: number) => number): number {
  return operation(a, b);
}

const add = (x: number, y: number): number => x + y;
const multiply = (x: number, y: number): number => x * y;

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

Loading code...

function calculate(a: number, b: number, operation: (x: number, y: number) => number): number {
  return operation(a, b);
}

const add = (x: number, y: number): number => x + y;
const multiply = (x: number, y: number): number => x * y;

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

Functions as return values

You can also type functions that are returned:

function multiply(factor: number): (value: number) => number {
  return (value: number) => value * factor;
}

const double = multiply(2);
console.log(double(10)); // 20

Loading code...

function multiply(factor: number): (value: number) => number {
  return (value: number) => value * factor;
}

const double = multiply(2);
console.log(double(10)); // 20

Functions with type annotations are ideal for building type-safe applications. Previously, we have briefly looked at union types, and we will cover these in more detail next.