How to Use Functions in TypeScript

By Raman Kumar

Updated on Jan 02, 2025

TypeScript enhances JavaScript with type safety, making it easier to write, read, and maintain code. Functions, a fundamental building block of any program, benefit greatly from TypeScript’s type annotations. This tutorial will guide you through creating typed functions, handling rest parameters, and using function overloading in TypeScript.

Use Functions in TypeScript

Basic Functions with Type Annotations

In TypeScript, you can add type annotations to function parameters and return types to enforce type safety. Let’s start by creating a simple function with type annotations:

function add(a: number, b: number): number {
  return a + b;
}
  • Parameters: a and b are explicitly defined as number.
  • Return Type: The return type number ensures the function always returns a numeric value.

When you try to pass arguments of incorrect types, TypeScript will throw an error during development, reducing runtime errors.

console.log(add(2, 3));  // Correct
console.log(add(2, "3")); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Optional Parameters

Optional parameters in TypeScript allow functions to accept arguments that may or may not be provided. You define them by adding a ? after the parameter name. This feature helps build flexible APIs while maintaining type safety.

Key Points:

  • Syntax: parameterName?: type
  • Default Value: If an optional parameter is omitted, its value is undefined.
  • Order: Optional parameters must follow required parameters in the function signature to avoid ambiguity.
function greet(name: string, greeting?: string): string {
  return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}

console.log(greet("Alice"));              // Output: Hello, Alice!
console.log(greet("Alice", "Good day"));  // Output: Good day, Alice!

Here, greeting is optional, and the function gracefully handles its absence.

Default Parameters

Default parameters are similar to optional parameters but allow you to specify a default value directly in the function signature. If the caller does not provide a value for a parameter, the default value is used.

Key Points:

  • Syntax: parameterName: type = defaultValue
  • Order: Default parameters can be mixed with required parameters but should ideally come last.
  • Evaluation: Default values can be simple constants or the result of a function call.
function multiply(a: number, b: number = 1): number {
  return a * b;
}

console.log(multiply(5));    // Output: 5
console.log(multiply(5, 2)); // Output: 10

Default parameters eliminate the need for null checks or explicit handling for missing values.

Rest Parameters

Rest parameters allow functions to accept a variable number of arguments and handle them as an array. They’re defined using the spread operator (...).

Key Points:

  • Type: Rest parameters must always have an array type (type[]).
  • Usage: Ideal for scenarios where the number of inputs is unknown or variable.
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // Output: 15
  • The rest parameter ...numbers collects all arguments into an array of number.
  • The reduce method processes the array and returns the sum.

Anonymous Functions and Arrow Functions

Anonymous functions are unnamed functions often assigned to variables or passed as arguments. Arrow functions (=>) are a concise syntax for writing anonymous functions and are especially useful in callbacks or functional programming.

Anonymous Function:

const divide = function (a: number, b: number): number {
  return a / b;
};
console.log(divide(10, 2)); // Output: 5

Arrow Function:

const subtract = (a: number, b: number): number => a - b;
console.log(subtract(10, 5)); // Output: 5

These syntaxes are compact and commonly used in modern TypeScript applications.

Function Overloading

Function overloading in TypeScript lets you define multiple signatures for a function. The function implementation must accommodate all overloads and use type guards or logic to differentiate between them.

Key Points:

  • Multiple Signatures: Define the possible combinations of arguments and their types.
  • Implementation: Use a single function implementation that handles all cases.
  • Type Guards: Employ checks like typeof to determine the argument types during execution.
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else {
    return value.toFixed(2);
  }
}

console.log(format("hello")); // Output: HELLO
console.log(format(42));      // Output: 42.00
  • The overloads define possible parameter types.
  • The implementation uses a union type (string | number) and includes type guards to differentiate between them.

Typed Callbacks

Typed callbacks are functions passed as arguments to other functions, with strict type definitions ensuring compatibility. This is especially useful for higher-order functions like map, filter, or reduce.

Key Points:

  • Type Signature: Specify the input and output types of the callback function.
  • Benefits: Prevents runtime errors by catching type mismatches during development.
function processNumbers(
  numbers: number[],
  callback: (num: number) => number
): number[] {
  return numbers.map(callback);
}

const doubled = processNumbers([1, 2, 3], (num) => num * 2);
console.log(doubled); // Output: [2, 4, 6]

Here:

  • callback is typed as a function that takes a number and returns a number.
  • TypeScript ensures the callback passed to processNumbers matches the expected type.

Higher-Order Functions

Higher-order functions return other functions or accept functions as arguments. Type annotations ensure type safety throughout the chain.

function createMultiplier(factor: number): (value: number) => number {
  return (value: number) => value * factor;
}

const triple = createMultiplier(3);
console.log(triple(10)); // Output: 30

Here:

  • createMultiplier returns a function with a specific type signature.
  • The returned function maintains type safety.

Advanced Usage: Generic Functions

Generics allow functions to work with multiple types while maintaining type safety.

function identity<T>(value: T): T {
  return value;
}

console.log(identity<number>(42));       // Output: 42
console.log(identity<string>("Hello")); // Output: Hello
  • T is a generic type that adapts to the type of the argument passed.
  • This makes the identity function flexible and reusable.

Advanced Usage of Functions in TypeScript

TypeScript provides several advanced features for creating and managing functions, enabling developers to write clean, efficient, and reusable code. Below are some advanced use cases and techniques for using functions in TypeScript.

1. Generic Functions

Generic functions allow you to create functions that work with a variety of types without sacrificing type safety. Generics are particularly useful for scenarios where the type is determined dynamically or depends on the inputs.

Example: Generic Identity Function

function identity<T>(value: T): T {
  return value;
}

console.log(identity<number>(42));      // Output: 42
console.log(identity<string>("TypeScript")); // Output: TypeScript

Explanation:

  • <T> is a type parameter that acts as a placeholder for a specific type.
  • The type is inferred or explicitly provided when the function is called.

Generic Constraints:

You can constrain generics using extends to restrict the types a generic can accept.

function logLength<T extends { length: number }>(item: T): void {
  console.log(item.length);
}

logLength("Hello"); // Output: 5
logLength([1, 2, 3]); // Output: 3

2. Higher-Order Functions

Higher-order functions are functions that take other functions as arguments or return functions. These are commonly used for callbacks, middleware, or functional programming.

Example: Function Returning a Function

function multiplier(factor: number): (value: number) => number {
  return (value: number) => value * factor;
}

const double = multiplier(2);

console.log(double(5)); // Output: 10

Example: Function Accepting a Callback

function processNumbers(
  numbers: number[],
  callback: (value: number) => number
): number[] {
  return numbers.map(callback);
}

const squaredNumbers = processNumbers([1, 2, 3], (num) => num * num);
console.log(squaredNumbers); // Output: [1, 4, 9]

3. Recursive Functions

Recursive functions are functions that call themselves to solve problems that can be divided into similar subproblems.

Example: Calculating Factorial

function factorial(n: number): number {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

console.log(factorial(5)); // Output: 120

Tail-Call Optimization

TypeScript supports tail-recursive functions where the recursive call is the last operation, making them memory-efficient.

function tailFactorial(n: number, accumulator: number = 1): number {
  if (n <= 1) return accumulator;
  return tailFactorial(n - 1, n * accumulator);
}

console.log(tailFactorial(5)); // Output: 120

4. Currying Functions

Currying is the process of transforming a function with multiple arguments into a series of functions that each take a single argument.

Example:

function add(a: number): (b: number) => number {
  return (b: number) => a + b;
}

const addFive = add(5);
console.log(addFive(3)); // Output: 8

Currying is useful for creating reusable and composable function pipelines.

5. Function Types

TypeScript allows you to define reusable function types to enforce consistency across multiple functions.

Example:

type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;

console.log(add(2, 3));       // Output: 5
console.log(multiply(2, 3));  // Output: 6

6. Callable and Constructable Types

TypeScript allows you to define types for objects that can be called as functions or used as constructors.

Callable Types:

type GreetFunction = {
  (name: string): string;
};

const greet: GreetFunction = (name) => `Hello, ${name}!`;

console.log(greet("Alice")); // Output: Hello, Alice!

Constructable Types:

type PersonConstructor = {
  new (name: string, age: number): { name: string; age: number };
};

const Person: PersonConstructor = class {
  constructor(public name: string, public age: number) {}
};

const john = new Person("John", 30);
console.log(john); // Output: { name: "John", age: 30 }

7. Intersection and Union Types with Functions

You can use intersection and union types to create flexible function signatures.

Example: Union Type

function format(input: string | number): string {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  return input.toString();
}

console.log(format("hello")); // Output: HELLO
console.log(format(123));     // Output: 123

Example: Intersection Type

type Logger = (message: string) => void;
type ErrorLogger = Logger & { logError: (error: Error) => void };

const errorLogger: ErrorLogger = Object.assign(
  (message: string) => console.log(message),
  {
    logError: (error: Error) => console.error(error.message),
  }
);

errorLogger("This is a log.");
errorLogger.logError(new Error("This is an error."));

8. Asynchronous Functions with async/await

TypeScript supports asynchronous functions with strict type definitions for promises.

Example:

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

fetchData("https://api.example.com").then((data) => console.log(data));

With async/await, you can write asynchronous code that is easier to read and debug.

9. Custom Utility Functions with keyof and infer

TypeScript's advanced type utilities enable you to create functions that operate on object keys or infer types dynamically.

Example: Function with keyof

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

const user = { name: "Alice", age: 25 };
console.log(pluck(user, "name")); // Output: Alice

Example: Infer Return Type

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;

function sum(a: number, b: number): number {
  return a + b;
}

type SumReturnType = ReturnTypeOf<typeof sum>; // Resolves to `number`

10. Decorator Functions

TypeScript decorators can modify or extend the behavior of functions or classes.

Example: Method Decorator

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Called ${propertyKey} with args:`, args);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calculator = new Calculator();
console.log(calculator.add(2, 3)); // Logs method call and output: 5

Conclusion

TypeScript provides robust tools for creating and working with functions. By leveraging type annotations, optional and default parameters, rest parameters, and function overloading, you can write safer and more maintainable code. Advanced features like generics and typed callbacks further enhance flexibility while maintaining type safety. Mastering these concepts will make your TypeScript functions powerful and versatile.

Advanced functions in TypeScript unlock powerful patterns for writing robust and reusable code. By combining features like generics, function types, decorators, and async/await, you can handle complex scenarios while maintaining type safety and clarity.

Checkout our instant dedicated servers and Instant KVM VPS plans.