1. javascript
  2. /typescript
  3. /functions

TypeScript Functions

Functions in TypeScript benefit from explicit type annotations for parameters and return values, enabling better error detection, IDE support, and self-documenting code. TypeScript's function typing system supports optional parameters, default values, rest parameters, overloads, and generic functions.

The type system ensures that functions are called with the correct arguments and that return values are properly handled, making your code more reliable and maintainable.

Basic Function Typing

TypeScript functions can have explicit types for parameters and return values, or rely on type inference.

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

// Type inference for return type
function multiply(x: number, y: number) {
  return x * y; // TypeScript infers return type as number
}

// Arrow function with explicit types
const subtract = (a: number, b: number): number => {
  return a - b;
};

// Arrow function with type inference
const divide = (x: number, y: number) => x / y;

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

// Function that never returns
function throwError(message: string): never {
  throw new Error(message);
}

// Usage
const result1 = add(5, 3); // result1: number
const result2 = multiply(4, 2); // result2: number
logMessage("Hello TypeScript");

Optional and Default Parameters

Parameters can be optional or have default values, providing flexibility in function calls.

// Optional parameters (must come after required parameters)
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// Default parameters
function createUser(name: string, age: number = 18, isActive: boolean = true): object {
  return { name, age, isActive };
}

// Multiple optional parameters
function formatDate(date: Date, includeTime?: boolean, timezone?: string): string {
  let formatted = date.toDateString();
  
  if (includeTime) {
    formatted += ` ${date.toTimeString()}`;
  }
  
  if (timezone) {
    formatted += ` (${timezone})`;
  }
  
  return formatted;
}

// Optional parameters with type unions
function parseValue(input: string, returnType?: "string" | "number"): string | number {
  if (returnType === "number") {
    return parseFloat(input);
  }
  return input;
}

// Usage
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob", "Hi")); // "Hi, Bob!"

const user1 = createUser("John"); // { name: "John", age: 18, isActive: true }
const user2 = createUser("Jane", 25, false); // { name: "Jane", age: 25, isActive: false }

Rest Parameters

Rest parameters allow functions to accept a variable number of arguments.

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

// Rest parameters with tuple types
function processValues(prefix: string, ...values: (string | number)[]): string[] {
  return values.map(value => `${prefix}: ${value}`);
}

// Rest parameters with specific tuple structure
function logMessages(level: string, ...messages: [string, ...string[]]): void {
  console.log(`[${level.toUpperCase()}]`, ...messages);
}

// Generic rest parameters
function combine<T>(...items: T[]): T[] {
  return items;
}

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

const processed = processValues("Item", "apple", 42, "banana");
// ["Item: apple", "Item: 42", "Item: banana"]

logMessages("info", "Application started", "Loading configuration");

const numbers = combine(1, 2, 3, 4); // number[]
const strings = combine("a", "b", "c"); // string[]

Function Overloads

Function overloads allow a single function to have multiple type signatures.

// Function overload signatures
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(flag: boolean): boolean;

// Implementation signature (must be compatible with all overloads)
function getValue(arg: number | string | boolean): string | number | boolean {
  if (typeof arg === "number") {
    return `Item ${arg}`;
  } else if (typeof arg === "string") {
    return arg.length;
  } else {
    return !arg;
  }
}

// More complex overload example
function parseData(data: string): object;
function parseData(data: string, format: "json"): object;
function parseData(data: string, format: "csv"): string[][];
function parseData(data: string, format: "xml"): Document;

function parseData(data: string, format?: "json" | "csv" | "xml"): object | string[][] | Document {
  switch (format) {
    case "json":
      return JSON.parse(data);
    case "csv":
      return data.split('\n').map(row => row.split(','));
    case "xml":
      return new DOMParser().parseFromString(data, "text/xml");
    default:
      return JSON.parse(data);
  }
}

// Usage - TypeScript knows the return type based on parameters
const stringResult = getValue(42); // string
const numberResult = getValue("hello"); // number
const boolResult = getValue(true); // boolean

const jsonData = parseData('{"name": "John"}'); // object
const csvData = parseData("a,b,c\n1,2,3", "csv"); // string[][]

Generic Functions

Generic functions work with multiple types while maintaining type safety.

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

// Generic function with multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

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

// Generic function with conditional logic
function wrapInArray<T>(value: T | T[]): T[] {
  return Array.isArray(value) ? value : [value];
}

// Generic function with default type parameters
function createCollection<T = string>(): T[] {
  return [];
}

// More complex generic example
function map<T, U>(array: T[], transform: (item: T) => U): U[] {
  return array.map(transform);
}

// Usage
const str = identity("hello"); // string
const num = identity(42); // number

const stringNumberPair = pair("age", 25); // [string, number]

const person = { name: "Alice", age: 30, city: "New York" };
const personName = getProperty(person, "name"); // string
const personAge = getProperty(person, "age"); // number

const wrapped1 = wrapInArray("hello"); // string[]
const wrapped2 = wrapInArray(["a", "b"]); // string[]

const stringCollection = createCollection(); // string[]
const numberCollection = createCollection<number>(); // number[]

const lengths = map(["hello", "world"], str => str.length); // number[]
const doubled = map([1, 2, 3], num => num * 2); // number[]

Function Types and Interfaces

Functions can be typed using function type expressions or interfaces.

// Function type expressions
type MathOperation = (a: number, b: number) => number;
type StringProcessor = (input: string) => string;
type Predicate<T> = (item: T) => boolean;

// Interface for function types
interface Calculator {
  add: (a: number, b: number) => number;
  subtract: (a: number, b: number) => number;
  multiply: (a: number, b: number) => number;
  divide: (a: number, b: number) => number;
}

// Interface for functions with call signatures
interface Formatter {
  (value: string): string;
  prefix: string;
  suffix: string;
}

// Using function types
const add: MathOperation = (x, y) => x + y;
const toUpperCase: StringProcessor = str => str.toUpperCase();
const isEven: Predicate<number> = num => num % 2 === 0;

// Implementing function interfaces
const calculator: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => b !== 0 ? a / b : 0
};

// Creating a function that matches a call signature interface
function createFormatter(prefix: string, suffix: string): Formatter {
  const formatter = (value: string): string => `${prefix}${value}${suffix}`;
  formatter.prefix = prefix;
  formatter.suffix = suffix;
  return formatter;
}

const htmlFormatter = createFormatter("<b>", "</b>");
console.log(htmlFormatter("Hello")); // "<b>Hello</b>"

Higher-Order Functions

Functions that take other functions as parameters or return functions.

// Function that takes a function as parameter
function withLogging<T extends any[], R>(
  fn: (...args: T) => R
): (...args: T) => R {
  return (...args: T): R => {
    console.log(`Calling function with args:`, args);
    const result = fn(...args);
    console.log(`Function returned:`, result);
    return result;
  };
}

// Function that returns a function
function createMultiplier(factor: number): (value: number) => number {
  return (value: number) => value * factor;
}

// Function that takes and returns functions
function compose<T, U, V>(
  f: (arg: U) => V,
  g: (arg: T) => U
): (arg: T) => V {
  return (arg: T) => f(g(arg));
}

// Array methods with proper typing
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
  return array.filter(predicate);
}

function reduce<T, U>(
  array: T[],
  reducer: (acc: U, current: T, index: number) => U,
  initialValue: U
): U {
  return array.reduce(reducer, initialValue);
}

// Usage
const loggedAdd = withLogging((a: number, b: number) => a + b);
const result = loggedAdd(5, 3); // Logs the call and result

const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(4)); // 12

const addOne = (x: number) => x + 1;
const toString = (x: number) => x.toString();
const addOneAndStringify = compose(toString, addOne);
console.log(addOneAndStringify(5)); // "6"

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filter(numbers, n => n % 2 === 0); // [2, 4]
const sum = reduce(numbers, (acc, curr) => acc + curr, 0); // 15

Async Functions

TypeScript provides full support for async/await with proper return type inference.

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

// Async function with explicit return type
async function getUser(id: number): Promise<{ id: number; name: string; email: string }> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch user: ${response.status}`);
  }
  return response.json();
}

// Generic async function
async function fetchMany<T>(urls: string[]): Promise<T[]> {
  const promises = urls.map(url => fetch(url).then(res => res.json()));
  return Promise.all(promises);
}

// Async function with error handling
async function safeApiCall<T>(
  apiCall: () => Promise<T>
): Promise<{ success: true; data: T } | { success: false; error: string }> {
  try {
    const data = await apiCall();
    return { success: true, data };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error.message : "Unknown error" 
    };
  }
}

// Async generator function
async function* generateNumbers(max: number): AsyncGenerator<number, void, unknown> {
  for (let i = 0; i < max; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

// Usage
async function examples() {
  try {
    const user = await getUser(1);
    console.log(user.name);

    const results = await fetchMany<{ title: string }>([
      "/api/posts/1",
      "/api/posts/2"
    ]);

    const apiResult = await safeApiCall(() => getUser(999));
    if (apiResult.success) {
      console.log(apiResult.data.name);
    } else {
      console.error(apiResult.error);
    }

    // Using async generator
    for await (const number of generateNumbers(5)) {
      console.log(number);
    }
  } catch (error) {
    console.error("Error:", error);
  }
}

Function Assertion and Type Guards

Functions can act as type guards to narrow types within conditional blocks.

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

function isNumber(value: unknown): value is number {
  return typeof value === "number";
}

function isUser(obj: any): obj is { id: number; name: string; email: string } {
  return obj && 
         typeof obj.id === "number" &&
         typeof obj.name === "string" &&
         typeof obj.email === "string";
}

// Generic type guard
function hasProperty<T, K extends string>(
  obj: T,
  key: K
): obj is T & Record<K, unknown> {
  return obj !== null && obj !== undefined && key in obj;
}

// Using type guards
function processValue(value: unknown): string {
  if (isString(value)) {
    return value.toUpperCase(); // TypeScript knows value is string
  }
  
  if (isNumber(value)) {
    return value.toFixed(2); // TypeScript knows value is number
  }
  
  return "Unknown type";
}

function processApiResponse(response: unknown): void {
  if (isUser(response)) {
    console.log(`User: ${response.name} (${response.email})`);
  } else {
    console.log("Invalid user data");
  }
}

// Array filter with type guards
const mixedArray: unknown[] = ["hello", 42, "world", true, 3.14];
const strings = mixedArray.filter(isString); // string[]
const numbers = mixedArray.filter(isNumber); // number[]

TypeScript's function typing system provides powerful tools for creating type-safe, self-documenting functions. From basic parameter typing to advanced generics and overloads, these features help build robust applications with clear interfaces and compile-time error detection.