Skip to main content

Functions & Generics

Typing Functions

Every function parameter should have a type. TypeScript infers the return type, but you can be explicit:

// Parameter types and inferred return type
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// Explicit return type
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Arrow functions work the same way
const multiply = (a: number, b: number): number => a * b;

Optional & Default Parameters

// Optional parameter (must come after required ones)
function createUser(name: string, age?: number) {
  return { name, age: age ?? 0 };
}

createUser("Alice");      // OK
createUser("Bob", 25);    // OK

// Default values (type is inferred from the default)
function paginate(items: string[], page = 1, pageSize = 10) {
  const start = (page - 1) * pageSize;
  return items.slice(start, start + pageSize);
}

Rest Parameters

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

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

Function Type Signatures

You can describe a function's shape as a type:

// Type alias for a function
type MathOp = (a: number, b: number) => number;

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

// As a callback parameter
function applyOp(a: number, b: number, op: MathOp): number {
  return op(a, b);
}

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

Overloaded Functions

Function overloads let a single function handle different parameter types:

// Overload signatures
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;

// Implementation signature (must handle all overloads)
function format(value: string | number | Date): string {
  if (typeof value === "string") return value.trim();
  if (typeof value === "number") return value.toFixed(2);
  return value.toISOString();
}

format("  hello  "); // "hello"
format(3.14159);      // "3.14"
format(new Date());   // "2026-03-12T00:00:00.000Z"

Introduction to Generics

Generics let you write functions that work with any type while still preserving type information:

// Without generics — loses type information
function firstElement(arr: any[]): any {
  return arr[0];
}
const x = firstElement([1, 2, 3]); // type: any (not useful)

// With generics — preserves the type
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const a = firstElement([1, 2, 3]);       // type: number
const b = firstElement(["a", "b", "c"]); // type: string

The <T> is a type parameter — a placeholder that gets filled in when the function is called.

Generic Constraints

Sometimes you need to restrict what types a generic accepts:

// T must have a 'length' property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");     // "hello"
longest([1, 2, 3], [4, 5]); // [1, 2, 3]
// longest(10, 20);          // Error: number doesn't have 'length'

Multiple Type Parameters

function mapObject<K extends string, V, R>(
  obj: Record<K, V>,
  fn: (value: V) => R
): Record<K, R> {
  const result = {} as Record<K, R>;
  for (const key in obj) {
    result[key] = fn(obj[key]);
  }
  return result;
}

const prices = { apple: 1.5, banana: 0.75, cherry: 3.0 };
const formatted = mapObject(prices, (p) => `$${p.toFixed(2)}`);
// { apple: "$1.50", banana: "$0.75", cherry: "$3.00" }

Built-in Utility Types

TypeScript includes utility types that transform existing types:

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

// Partial — all properties become optional
type UpdateUserDto = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number }

// Required — all properties become required
type RequiredUser = Required<Partial<User>>;

// Pick — select specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: string; name: string }

// Omit — remove specific properties
type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; age: number }

// Readonly — all properties become readonly
type FrozenUser = Readonly<User>;

Practical Example: A Generic API Client

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

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  return res.json();
}

// Usage — the type flows through automatically
interface Product {
  id: string;
  name: string;
  price: number;
}

const response = await fetchApi<Product[]>("/api/products");
// response.data is Product[] — fully typed
response.data.forEach((p) => console.log(p.name, p.price));