In the previous tutorial, we learned the basic types in TypeScript. Now let’s learn how to use types with functions — the building blocks of every program.

By the end of this tutorial, you will know how to type every kind of function in TypeScript.

Function Parameter Types

In TypeScript, you must add types to function parameters. TypeScript cannot infer parameter types:

// JavaScript — no types
function greet(name) {
  return "Hello, " + name;
}

// TypeScript — types required
function greet(name: string) {
  return "Hello, " + name;
}

If you forget to add a type, TypeScript gives an error with strict: true:

function greet(name) {
  // Error: Parameter 'name' implicitly has an 'any' type
  return "Hello, " + name;
}

Multiple parameters work the same way:

function add(a: number, b: number) {
  return a + b;
}

add(5, 3);      // OK
add("5", 3);    // Error: Argument of type 'string' is not assignable to parameter of type 'number'

Return Type Annotations

You can add a return type after the parameter list:

function add(a: number, b: number): number {
  return a + b;
}

TypeScript can usually infer the return type from the code. So you don’t always need to write it:

// TypeScript infers return type as number
function add(a: number, b: number) {
  return a + b;
}

When should you add return types?

  • Public API functions — makes the contract clear
  • Complex functions — helps catch errors early
  • When the inferred type is wrong — explicit types override inference

For simple functions, inference is fine. For functions others will use, explicit return types are safer.

Arrow Functions

Arrow functions use the same type syntax:

const add = (a: number, b: number): number => {
  return a + b;
};

// Short form for one-line functions
const multiply = (a: number, b: number): number => a * b;

TypeScript infers the type of the arrow function variable. You can also type the variable explicitly:

const add: (a: number, b: number) => number = (a, b) => {
  return a + b;
};

This is more verbose but useful when you want to define the function type separately (we cover this later in the callback types section).

Optional Parameters

Add a ? after the parameter name to make it optional:

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

greet("Alex");            // "Hello, Alex!"
greet("Alex", "Good morning"); // "Good morning, Alex!"

Optional parameters are undefined when not provided. They must come after required parameters:

// Error: A required parameter cannot follow an optional parameter
function greet(greeting?: string, name: string) {
  // ...
}

The type of an optional parameter is automatically a union with undefined. So greeting?: string is the same as greeting: string | undefined.

Default Parameters

Default parameters provide a fallback value:

function greet(name: string, greeting: string = "Hello") {
  return `${greeting}, ${name}!`;
}

greet("Alex");           // "Hello, Alex!"
greet("Alex", "Hi");     // "Hi, Alex!"

TypeScript infers the type from the default value. You don’t need to add : string when the default is a string.

Default parameters are different from optional parameters:

FeatureOptional (?)Default (= value)
Value when omittedundefinedThe default value
Type includes undefinedYesNo (unless default is undefined)
Must be last?YesNo (but usually last)

Rest Parameters

Rest parameters collect multiple arguments into an array:

function sum(...numbers: number[]): number {
  let total = 0;
  for (const num of numbers) {
    total += num;
  }
  return total;
}

sum(1, 2, 3);       // 6
sum(10, 20, 30, 40); // 100

The ...numbers: number[] means “collect all arguments into a number[] array.”

You can combine rest parameters with regular parameters:

function log(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, ...messages);
}

log("INFO", "Server started", "Port 3000");
// [INFO] Server started Port 3000

Rest parameters must always be the last parameter.

void Return Type

Use void when a function does not return anything:

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

A void function can have a return statement with no value, or no return at all.

never Return Type

Use never when a function never returns — it either throws or loops forever:

function throwError(message: string): never {
  throw new Error(message);
}

function fail(message: string): never {
  throw new Error(message);
}

The difference between void and never:

Return TypeMeaningExample
voidFunction returns, but with no valueconsole.log()
neverFunction never reaches the endthrow new Error()

Function Overloads

Sometimes a function accepts different types of arguments and returns different types. Use overloads to describe this:

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

format("hello"); // "HELLO" — TypeScript knows the return is string
format(3.14159); // "3.14" — TypeScript knows the return is string

The first two lines are overload signatures. They tell TypeScript what combinations of types are allowed. The last function is the implementation — it handles all cases.

When you call format, TypeScript matches your arguments against the overload signatures, not the implementation.

When to Use Overloads

Use overloads when the return type depends on the input type:

function first(arr: string[]): string;
function first(arr: number[]): number;
function first(arr: (string | number)[]): string | number {
  return arr[0];
}

const name = first(["Alex", "Sam"]);  // type: string
const score = first([95, 87, 92]);     // type: number

Without overloads, the return type would be string | number for both calls. Overloads give TypeScript more precise information.

Tip: If you can use a generic instead of overloads, prefer the generic. Generics are simpler. We cover generics in a later tutorial.

Callback Types

You can type functions that accept other functions as arguments:

function doTwice(action: (value: string) => void, value: string): void {
  action(value);
  action(value);
}

doTwice(console.log, "Hello!");
// Hello!
// Hello!

The type (value: string) => void describes a function that takes a string and returns nothing.

Type Aliases for Callbacks

For complex callback types, use a type alias:

type FilterFn = (item: string) => boolean;

function filterList(items: string[], predicate: FilterFn): string[] {
  const result: string[] = [];
  for (const item of items) {
    if (predicate(item)) {
      result.push(item);
    }
  }
  return result;
}

const long = filterList(
  ["hi", "hello", "hey there"],
  (item) => item.length > 3
);
// ["hello", "hey there"]

Notice that the callback (item) => item.length > 3 does not need type annotations. TypeScript infers the type from the FilterFn type alias. This is called contextual typing.

Typing Common Patterns

Processing Functions

function processItems(
  items: string[],
  transform: (item: string) => string
): string[] {
  return items.map(transform);
}

const upper = processItems(["hello", "world"], (s) => s.toUpperCase());
// ["HELLO", "WORLD"]

Event Handlers

type EventHandler = (event: { type: string; data: unknown }) => void;

function onEvent(handler: EventHandler): void {
  // Register the handler
  handler({ type: "click", data: null });
}

Async Functions

Async functions return a Promise:

async function fetchUser(id: number): Promise<string> {
  // Simulate API call
  return `User ${id}`;
}

The return type is Promise<string>, not just string. We will cover async patterns in detail in a later tutorial.

Quick Practice

Create src/functions.ts and try these:

// 1. Basic function with types
function repeat(text: string, times: number): string {
  return text.repeat(times);
}
console.log(repeat("ha", 3)); // "hahaha"

// 2. Arrow function with optional parameter
const greet = (name: string, loud?: boolean): string => {
  const message = `Hello, ${name}!`;
  return loud ? message.toUpperCase() : message;
};
console.log(greet("Alex"));        // "Hello, Alex!"
console.log(greet("Alex", true));  // "HELLO, ALEX!"

// 3. Rest parameters
function joinWords(...words: string[]): string {
  return words.join(" ");
}
console.log(joinWords("TypeScript", "is", "great")); // "TypeScript is great"

// 4. Callback function
function applyToAll(
  items: number[],
  fn: (n: number) => number
): number[] {
  return items.map(fn);
}
const doubled = applyToAll([1, 2, 3], (n) => n * 2);
console.log(doubled); // [2, 4, 6]

Run it:

npx tsx src/functions.ts

What is Next?

In the next tutorial, we will learn about objects and interfaces — how to define the shape of complex data in TypeScript.