Skip to main content

Functions & Types

Functions are the building blocks of any application. TypeScript makes functions safer by typing parameters, return values, and function signatures themselves.

1. Function Type Annotations

Parameter and Return Types

// Annotate parameters and return type
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function
const multiply = (a: number, b: number): number => a * b;

// Return type is often inferred
const divide = (a: number, b: number) => a / b; // Returns number

void and never

// void - function doesn't return anything
function log(message: string): void {
  console.log(message);
}

// never - function never returns (throws or infinite loop)
function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

2. Optional and Default Parameters

Optional Parameters

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

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

Default Parameters

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

greet('Alice');           // 'Hello, Alice!'
greet('Alice', 'Hi');     // 'Hi, Alice!'
Optional vs Default: Optional parameters can be undefined. Default parameters have a fallback value. Default parameters are usually preferred.

Rest Parameters

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);        // 6
sum(1, 2, 3, 4, 5);  // 15

3. Function Type Expressions

Define the type of a function itself.
// Type alias for a function
type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;

// As a parameter
function calculate(a: number, b: number, operation: MathOperation): number {
  return operation(a, b);
}

calculate(10, 5, add);      // 15
calculate(10, 5, subtract); // 5

Call Signatures

// Object type with call signature
type DescribableFunction = {
  description: string;
  (x: number): number;
};

function double(x: number): number {
  return x * 2;
}
double.description = 'Doubles the input';

const fn: DescribableFunction = double;

Construct Signatures

// For classes/constructors
type UserConstructor = {
  new (name: string): User;
};

class User {
  constructor(public name: string) {}
}

function createUser(ctor: UserConstructor, name: string): User {
  return new ctor(name);
}

const user = createUser(User, 'Alice');

4. Function Overloads

Define multiple function signatures for different parameter combinations.
// Overload signatures
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;

// Implementation signature
function format(value: string | number | Date): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  } else if (typeof value === 'number') {
    return value.toFixed(2);
  } else {
    return value.toISOString();
  }
}

format('hello');           // 'HELLO'
format(3.14159);          // '3.14'
format(new Date());       // '2024-01-01T00:00:00.000Z'

More Complex Overloads

// Return different types based on input
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: string): HTMLElement;

function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const div = createElement('div');    // HTMLDivElement
const span = createElement('span');  // HTMLSpanElement
const input = createElement('input'); // HTMLInputElement
const custom = createElement('custom'); // HTMLElement
Overloads Rule: The implementation signature must be compatible with all overload signatures but is not visible to callers.

5. Generic Functions

Write functions that work with any type while maintaining type safety.

Basic Generics

// Without generics
function identity(value: any): any {
  return value;
}

// With generics - type is preserved!
function identity<T>(value: T): T {
  return value;
}

const num = identity(42);        // number
const str = identity('hello');   // string
const obj = identity({ x: 1 });  // { x: number }

Multiple Type Parameters

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair('hello', 42);           // [string, number]
const p2 = pair(true, { name: 'Alice' }); // [boolean, { name: string }]

Generic Constraints

// Constrain T to have a length property
function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength('hello');     // 5
getLength([1, 2, 3]);   // 3
getLength({ length: 10 }); // 10
// getLength(123);      // ❌ Error: number doesn't have length

keyof Constraint

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 25 };

getProperty(user, 'name'); // string
getProperty(user, 'age');  // number
// getProperty(user, 'email'); // ❌ Error: 'email' is not a key of user

6. Type Guards

Functions that narrow types at runtime.

typeof Guards

function processValue(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  return value.toString();
}

instanceof Guards

class Dog {
  bark() { console.log('Woof!'); }
}

class Cat {
  meow() { console.log('Meow!'); }
}

function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

Custom Type Guards (Type Predicates)

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

// Custom type guard with 'is' keyword
function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

function move(animal: Fish | Bird): void {
  if (isFish(animal)) {
    animal.swim(); // TypeScript knows it's Fish
  } else {
    animal.fly();  // TypeScript knows it's Bird
  }
}

Discriminated Unions

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}

const circle: Shape = { kind: 'circle', radius: 5 };
getArea(circle); // 78.54

7. Assertion Functions

Functions that throw if a condition is false, narrowing the type.
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Value must be a string');
  }
}

function processInput(input: unknown): string {
  assertIsString(input);
  // TypeScript knows input is string after assertion
  return input.toUpperCase();
}

Non-null Assertion

function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error('Value must be defined');
  }
}

function getUser(id: string): User | null {
  // ... fetch user
  return null;
}

const user = getUser('123');
assertDefined(user);
// TypeScript knows user is User (not null) after this point
console.log(user.name);

8. this Parameter

Explicitly type the this context.
interface User {
  name: string;
  greet(this: User): string;
}

const user: User = {
  name: 'Alice',
  greet() {
    return `Hello, ${this.name}!`;
  }
};

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

// This would error because 'this' context is wrong
const greet = user.greet;
// greet(); // ❌ Error: 'this' context of type 'void' is not assignable

This Parameter in Classes

class Counter {
  count = 0;

  // Arrow function preserves 'this'
  increment = () => {
    this.count++;
  };

  // Regular method with explicit this type
  decrement(this: Counter): void {
    this.count--;
  }
}

9. Callback Types

Type callbacks precisely.
// Simple callback
type Callback = (result: string) => void;

function fetchData(callback: Callback): void {
  setTimeout(() => {
    callback('Data loaded');
  }, 1000);
}

// Callback with error handling
type NodeCallback<T> = (error: Error | null, result: T | null) => void;

function readFile(path: string, callback: NodeCallback<string>): void {
  // ...
}

// Event handler
type EventHandler<E extends Event> = (event: E) => void;

const handleClick: EventHandler<MouseEvent> = (event) => {
  console.log(event.clientX, event.clientY);
};

10. Practical Examples

API Function Types

interface User {
  id: number;
  name: string;
  email: string;
}

type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
};

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const response = await fetch('/api/users');
  return response.json();
}

Event Emitter Pattern

type EventMap = {
  login: { userId: string };
  logout: { userId: string };
  error: { message: string; code: number };
};

type EventCallback<T> = (data: T) => void;

class EventEmitter<T extends Record<string, any>> {
  private listeners = new Map<keyof T, Set<EventCallback<any>>>();

  on<K extends keyof T>(event: K, callback: EventCallback<T[K]>): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach((callback) => callback(data));
  }
}

const emitter = new EventEmitter<EventMap>();

emitter.on('login', (data) => {
  console.log(`User ${data.userId} logged in`);
});

emitter.emit('login', { userId: '123' }); // Type-safe!

Summary

ConceptExample
Parameter Typesfunction add(a: number, b: number)
Return Typesfunction add(a, b): number
Optional Paramsfunction greet(name: string, greeting?: string)
Default Paramsfunction greet(name: string, greeting = 'Hello')
Rest Paramsfunction sum(...nums: number[])
Function Typestype Fn = (x: number) => number
OverloadsMultiple signatures + implementation
Genericsfunction identity<T>(value: T): T
Type Guardsfunction isFish(x): x is Fish
Assertionsfunction assert(x): asserts x is string
Next, we’ll explore objects and interfaces in depth!