beginner Step 5 of 18

Functions with Types

TypeScript Programming

Functions with Types

Functions are the fundamental building blocks of any TypeScript application. TypeScript enhances JavaScript functions with type annotations for parameters and return values, ensuring that functions are called with the correct arguments and that their results are used appropriately. This compile-time checking eliminates a huge class of bugs that would otherwise only surface at runtime. TypeScript also supports optional parameters, default values, rest parameters, and function overloads, giving you precise control over how your functions can be invoked.

Parameter and Return Types

Type annotations are placed after each parameter name and after the parameter list for the return type. While TypeScript can often infer return types, explicitly declaring them is considered best practice because it documents intent and catches accidental changes.

// Explicit parameter and return types
function add(a: number, b: number): number {
  return a + b;
}

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

// String function
function formatName(first: string, last: string): string {
  return `${first} ${last}`;
}

// Boolean return
function isEven(n: number): boolean {
  return n % 2 === 0;
}

// Void return — function does not return a value
function logInfo(message: string): void {
  console.log(`[INFO] ${message}`);
}

Optional and Default Parameters

Optional parameters are marked with a ? after the parameter name. They must come after all required parameters. Default parameters provide a fallback value and are inherently optional.

// Optional parameter
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}
greet("Alice");            // "Hello, Alice!"
greet("Alice", "Welcome"); // "Welcome, Alice!"

// Default parameter
function createUser(
  name: string,
  role: string = "viewer",
  active: boolean = true
): { name: string; role: string; active: boolean } {
  return { name, role, active };
}
createUser("Bob");                    // { name: "Bob", role: "viewer", active: true }
createUser("Charlie", "admin");       // { name: "Charlie", role: "admin", active: true }
createUser("Diana", "editor", false); // { name: "Diana", role: "editor", active: false }

Rest Parameters

Rest parameters collect all remaining arguments into a typed array. They must be the last parameter in the function signature.

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

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

// Combine regular and rest parameters
function logTagged(tag: string, ...messages: string[]): void {
  messages.forEach(msg => console.log(`[${tag}] ${msg}`));
}
logTagged("DEBUG", "Starting process", "Loading config", "Ready");

Function Types

You can define the type of a function variable using function type expressions. This is useful for callbacks, higher-order functions, and defining interfaces that include methods.

// Function type expression
let calculator: (a: number, b: number) => number;

calculator = (x, y) => x + y;
console.log(calculator(5, 3)); // 8

calculator = (x, y) => x * y;
console.log(calculator(5, 3)); // 15

// Type alias for a function type
type Predicate = (value: number) => boolean;

function filterNumbers(nums: number[], test: Predicate): number[] {
  return nums.filter(test);
}

const evens = filterNumbers([1, 2, 3, 4, 5, 6], n => n % 2 === 0);
console.log(evens); // [2, 4, 6]

Function Overloads

Function overloads let you define multiple call signatures for a single function. The implementation signature must be compatible with all overload signatures. This is useful when a function behaves differently based on the types or number of arguments.

// 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.trim().toUpperCase();
  } else if (typeof value === "number") {
    return value.toFixed(2);
  } else {
    return value.toISOString().split("T")[0];
  }
}

console.log(format("  hello  "));       // "HELLO"
console.log(format(3.14159));            // "3.14"
console.log(format(new Date(2024, 0, 1))); // "2024-01-01"
Tip: Prefer union types over function overloads when the implementation logic does not differ significantly between signatures. Overloads add complexity and should be reserved for cases where the return type depends on the input type.

Key Takeaways

  • Annotate function parameters and return types explicitly for clarity and safety.
  • Use ? for optional parameters and provide default values with =.
  • Rest parameters (...args: T[]) collect variable arguments into a typed array.
  • Function type expressions like (a: number) => string describe the shape of callable values.
  • Function overloads define multiple call signatures when behavior varies by argument type.