Skip to content

Functions#

Functions are the building blocks of any application. TypeScript adds type safety to function parameters and return values.

Function Type Annotations#

Basic Function Types#

// Named function with types
function add(x: number, y: number): number {
  return x + y;
}

// Arrow function with types
const multiply = (x: number, y: number): number => {
  return x * y;
};

// Short arrow function
const subtract = (x: number, y: number): number => x - y;

// Function with no return value
function logMessage(message: string): void {
  console.log(message);
}

Type Inference on Return Types#

// TypeScript can infer the return type
function divide(x: number, y: number) {
  return x / y; // Inferred as number
}

// But explicit is often better for documentation
function getUser(id: string): User {
  // Return type makes intent clear
  return { id, name: "John" };
}

Optional and Default Parameters#

Optional Parameters#

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  }
  return firstName;
}

console.log(buildName("John"));           // "John"
console.log(buildName("John", "Doe"));    // "John Doe"

// Optional parameters must come after required ones
function greet(greeting?: string, name: string): string { // ❌ Error
  return `${greeting} ${name}`;
}

Default Parameters#

function createUser(
  name: string,
  role: string = "user",
  isActive: boolean = true
): User {
  return { name, role, isActive };
}

createUser("Alice");                    // role: "user", isActive: true
createUser("Bob", "admin");             // role: "admin", isActive: true
createUser("Charlie", "user", false);   // role: "user", isActive: false

// Default parameters can use complex expressions
function greet(name: string, greeting: string = getDefaultGreeting()): string {
  return `${greeting}, ${name}!`;
}

Rest Parameters#

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

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

// Rest parameters with other parameters
function multiply(multiplier: number, ...numbers: number[]): number[] {
  return numbers.map(n => n * multiplier);
}

console.log(multiply(2, 1, 2, 3));  // [2, 4, 6]

// Typed tuple rest parameters
function createUser(
  name: string,
  ...roles: [string, ...string[]]
): User {
  return { name, roles };
}

createUser("Alice", "admin");              // At least one role required
createUser("Bob", "user", "moderator");    // Multiple roles OK

Function Overloads#

Define multiple function signatures for the same function:

// Overload signatures
function getValue(id: number): string;
function getValue(name: string): number;

// Implementation signature
function getValue(idOrName: number | string): string | number {
  if (typeof idOrName === "number") {
    return `ID: ${idOrName}`;
  } else {
    return idOrName.length;
  }
}

const result1 = getValue(42);      // Type: string
const result2 = getValue("Alice"); // Type: number

// Real-world example: createElement
function createElement(tag: "img"): HTMLImageElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement;

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

const img = createElement("img");    // Type: HTMLImageElement
const input = createElement("input"); // Type: HTMLInputElement
const div = createElement("div");     // Type: HTMLElement

Function Types#

Function Type Expressions#

// Function type
type MathOperation = (x: number, y: number) => number;

const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;

// Using in other types
type Calculator = {
  operation: MathOperation;
  name: string;
};

// Callback function type
type Callback = (error: Error | null, data: any) => void;

function fetchData(url: string, callback: Callback): void {
  // ...
}

Call Signatures#

// More detailed function types
type DescribableFunction = {
  description: string;
  (value: number): boolean; // Call signature
};

function doSomething(fn: DescribableFunction): void {
  console.log(fn.description + " returned " + fn(6));
}

function isEven(value: number): boolean {
  return value % 2 === 0;
}

isEven.description = "Check if number is even";

doSomething(isEven);

Construct Signatures#

// Constructor function type
type Constructor = {
  new (name: string): { name: string };
};

function createInstance(Ctor: Constructor, name: string) {
  return new Ctor(name);
}

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

const person = createInstance(Person, "Alice");

Generic Functions#

Write reusable functions that work with multiple types:

// Basic generic function
function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(42);      // Type: number
const str = identity<string>("hello"); // Type: string
const auto = identity(true);           // Type inference: boolean

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

const person = { name: "Alice", age: 25 };
const name = getProperty(person, "name"); // Type: string
const age = getProperty(person, "age");   // Type: number
// getProperty(person, "invalid");        // ❌ Error

// Multiple type parameters
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const result = merge({ name: "Alice" }, { age: 25 });
// result has type: { name: string } & { age: number }

// Generic with default type
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const strings = createArray(3, "hello"); // Type: string[]
const numbers = createArray<number>(3, 0); // Type: number[]

Arrow Functions#

// Basic arrow function
const greet = (name: string): string => `Hello, ${name}!`;

// Multi-line arrow function
const processUser = (user: User): string => {
  const fullName = `${user.firstName} ${user.lastName}`;
  return fullName.toUpperCase();
};

// Arrow function as callback
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n: number): number => n * 2);

// this context preservation
class Counter {
  count = 0;

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

  // Regular function - 'this' can change
  decrement(): void {
    this.count--;
  }
}

Async Functions#

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

// Using the async function
async function displayUser(id: string): Promise<void> {
  try {
    const user = await fetchUser(id);
    console.log(user);
  } catch (error) {
    console.error("Failed to fetch user:", error);
  }
}

// Arrow async function
const fetchData = async (url: string): Promise<any> => {
  const response = await fetch(url);
  return response.json();
};

// Generic async function
async function fetchItems<T>(url: string): Promise<T[]> {
  const response = await fetch(url);
  return response.json();
}

const users = await fetchItems<User>("/api/users");
const products = await fetchItems<Product>("/api/products");

Callback Functions#

// Simple callback
function processArray(
  arr: number[],
  callback: (item: number) => number
): number[] {
  return arr.map(callback);
}

processArray([1, 2, 3], n => n * 2); // [2, 4, 6]

// Node.js style callbacks
type NodeCallback<T> = (error: Error | null, data?: T) => void;

function readFile(path: string, callback: NodeCallback<string>): void {
  // Simulated async operation
  setTimeout(() => {
    if (path) {
      callback(null, "file contents");
    } else {
      callback(new Error("Invalid path"));
    }
  }, 100);
}

// Event handler callback
type EventHandler = (event: MouseEvent) => void;

function addClickListener(element: HTMLElement, handler: EventHandler): void {
  element.addEventListener("click", handler);
}

Function Type Guards#

// Type predicate
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function processValue(value: string | number): void {
  if (isString(value)) {
    console.log(value.toUpperCase()); // TypeScript knows it's a string
  } else {
    console.log(value.toFixed(2)); // TypeScript knows it's a number
  }
}

// Custom type guard for objects
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

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

Practical Examples#

API Request Handler#

type ApiResponse<T> = {
  data: T | null;
  error: string | null;
  status: number;
};

async function apiRequest<T>(
  url: string,
  options?: RequestInit
): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url, options);
    const data = await response.json();

    return {
      data,
      error: null,
      status: response.status
    };
  } catch (error) {
    return {
      data: null,
      error: error instanceof Error ? error.message : "Unknown error",
      status: 500
    };
  }
}

// Usage
const result = await apiRequest<User>("/api/user/123");
if (result.error) {
  console.error(result.error);
} else {
  console.log(result.data);
}

Event Emitter#

type EventHandler<T = any> = (data: T) => void;

class EventEmitter<Events extends Record<string, any>> {
  private listeners: Map<keyof Events, EventHandler[]> = new Map();

  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {
    const handlers = this.listeners.get(event) || [];
    handlers.push(handler);
    this.listeners.set(event, handlers);
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    const handlers = this.listeners.get(event) || [];
    handlers.forEach(handler => handler(data));
  }
}

// Usage
type AppEvents = {
  userLogin: { userId: string; timestamp: number };
  userLogout: { userId: string };
  error: { message: string; code: number };
};

const emitter = new EventEmitter<AppEvents>();

emitter.on("userLogin", (data) => {
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

emitter.emit("userLogin", { userId: "123", timestamp: Date.now() });

Validation Function#

type ValidationRule<T> = (value: T) => string | null;

function createValidator<T>(
  rules: ValidationRule<T>[]
): (value: T) => string[] {
  return (value: T) => {
    const errors: string[] = [];

    for (const rule of rules) {
      const error = rule(value);
      if (error) {
        errors.push(error);
      }
    }

    return errors;
  };
}

// Usage
const validatePassword = createValidator<string>([
  (pwd) => pwd.length < 8 ? "Password must be at least 8 characters" : null,
  (pwd) => !/[A-Z]/.test(pwd) ? "Password must contain uppercase letter" : null,
  (pwd) => !/[0-9]/.test(pwd) ? "Password must contain a number" : null,
]);

const errors = validatePassword("weak");
console.log(errors); // ["Password must be at least 8 characters", ...]

Best Practices#

  1. Always type function parameters

    function greet(name: string): string { /* ... */ }
    

  2. Return type can often be inferred, but explicit is clearer

    function getUser(id: string): Promise<User> { /* ... */ }
    

  3. Use optional parameters instead of undefined unions

    function buildName(first: string, last?: string) { /* ... */ }
    // Instead of: (first: string, last: string | undefined)
    

  4. Use function overloads for truly different behaviors

    function process(x: number): string;
    function process(x: string): number;
    

  5. Prefer type predicates for type guards

    function isString(x: unknown): x is string { /* ... */ }
    

Next Steps#