Pass Functions as Parameters in TypeScript

Passing functions as parameters is one of those TypeScript skills that transforms how you write code. I’ve been working with TypeScript for over 5 years in enterprise applications, and this pattern shows up everywhere, from React hooks to API callbacks to custom validators in SharePoint Framework projects.

In this guide, I’ll walk you through everything you need to know about passing functions as parameters in TypeScript. You’ll learn the core syntax, type safety patterns, real-world use cases, and common mistakes to avoid.

What Does “Pass a Function as a Parameter” Mean?

In TypeScript and JavaScript, functions are first-class citizens. This means you can treat them like any other value, assign them to variables, store them in arrays, and yes, pass them as parameters to other functions.

Here’s the simplest example:

function sayHello(name: string): string {
return `Hello, ${name}!`;
}

function executeGreeting(greetingFunc: (name: string) => string, userName: string): void {
console.log(greetingFunc(userName));
}

executeGreeting(sayHello, "Michael");
// Output: Hello, Michael!

You can see the output in the screenshot below.

TypeScript Pass Functions as Parameters

Notice the type annotation (name: string) => string in the executeGreeting function. This tells TypeScript exactly what kind of function we expect, one that takes a string parameter and returns a string.

Why Type Annotations Matter for Function Parameters

When I first moved from JavaScript to TypeScript, I learned this lesson the hard way. Without proper type annotations, you lose all the safety TypeScript provides.

The Problem Without Types

// JavaScript-style code (avoid this)
function processData(callback, data) {
return callback(data);
}

processData(123, "some data"); // No error, but will crash at runtime!

The TypeScript Solution

function processData(callback: (data: string) => string, data: string): string {
return callback(data);
}

processData(123, "some data");
// Error: Argument of type 'number' is not assignable to parameter of type '(data: string) => string'

TypeScript catches the error before your code even runs. This is huge when you’re building production applications.

Basic Function Parameter Syntax

Let me show you the fundamental patterns you’ll use most often.

Method 1: Inline Function Type

function greetUser(name: string): string {
return `Hey ${name}, welcome back!`;
}

function displayMessage(
messageFunc: (name: string) => string,
userName: string
): void {
const message = messageFunc(userName);
console.log(message);
}

displayMessage(greetUser, "Sarah");
// Output: Hey Sarah, welcome back!

You can see the output in the screenshot below.

Pass Functions as Parameters TypeScript

The syntax (name: string) => string defines:

  • Parameter type: (name: string)
  • Return type: string

Method 2: Type Alias for Reusability

When you use the same function signature multiple times, create a type alias:

type GreetingFunction = (name: string) => string;

function logGreeting(
greetingFunc: GreetingFunction,
userName: string
): void {
console.log(greetingFunc(userName));
}

function sendGreeting(
greetingFunc: GreetingFunction,
userName: string
): string {
return greetingFunc(userName);
}

I use this pattern constantly in SharePoint Framework development, where you have consistent service patterns across multiple components.

Method 3: Interface for Complex Function Signatures

For more complex scenarios, interfaces work great:

interface DataProcessor {
(data: string[], options: { uppercase: boolean }): string[];
}

const formatNames: DataProcessor = (data, options) => {
return options.uppercase
? data.map(name => name.toUpperCase())
: data;
};

function processUserList(
processor: DataProcessor,
names: string[]
): string[] {
return processor(names, { uppercase: true });
}

const users = ["emily", "david", "jessica"];
console.log(processUserList(formatNames, users));
// Output: ["EMILY", "DAVID", "JESSICA"]

You can see the output in the screenshot below.

Pass Functions as Parameters in TypeScript

Real-World Example: Callback Functions for Async Operations

This is where passing functions as parameters really shines. Let me show you a pattern I use in almost every project.

Basic Callback Pattern

interface User {
firstName: string;
lastName: string;
email: string;
}

function fetchUserData(
userId: number,
onSuccess: (user: User) => void
): void {
// Simulate API call
setTimeout(() => {
const user: User = {
firstName: "Robert",
lastName: "Martinez",
email: "robert.martinez@example.com"
};
onSuccess(user);
}, 1000);
}

function displayUser(user: User): void {
console.log(`User: ${user.firstName} ${user.lastName}`);
console.log(`Email: ${user.email}`);
}

fetchUserData(101, displayUser);

Enhanced Callback with Error Handling

In real applications, you need to handle failures. Here’s the pattern I actually use:

type SuccessCallback<T> = (data: T) => void;
type ErrorCallback = (error: Error) => void;

function fetchUserData(
userId: number,
onSuccess: SuccessCallback<User>,
onError: ErrorCallback
): void {
setTimeout(() => {
try {
if (userId <= 0) {
throw new Error("Invalid user ID");
}

const user: User = {
firstName: "Jennifer",
lastName: "Thompson",
email: "jennifer.thompson@example.com"
};
onSuccess(user);
} catch (error) {
onError(error as Error);
}
}, 1000);
}

// Usage
fetchUserData(
101,
(user) => console.log(`Success: ${user.firstName}`),
(error) => console.error(`Failed: ${error.message}`)
);

This pattern is essential for building robust API integrations in Power Platform custom connectors.

Multiple Parameters in Function Parameters

Sometimes the function you’re passing needs multiple arguments. Here’s how to handle that cleanly:

type CalculationFunction = (price: number, taxRate: number) => number;

function calculateTotal(price: number, taxRate: number): number {
return price + (price * taxRate);
}

function processTransaction(
calculator: CalculationFunction,
itemPrice: number,
tax: number
): void {
const total = calculator(itemPrice, tax);
console.log(`Item price: $${itemPrice.toFixed(2)}`);
console.log(`Tax: ${(tax * 100).toFixed(1)}%`);
console.log(`Total: $${total.toFixed(2)}`);
}

processTransaction(calculateTotal, 99.99, 0.0825);
// Output:
// Item price: $99.99
// Tax: 8.2%
// Total: $108.24

This pattern is super useful when building validation functions for Power Apps component frameworks where you need flexible calculation logic.

Arrow Functions as Parameters

Arrow functions give you cleaner, more concise syntax. I use them all the time for simple operations:

const products = ["laptop", "mouse", "keyboard", "monitor"];

function processItems(
items: string[],
processor: (item: string, index: number) => void
): void {
items.forEach((item, idx) => processor(item, idx));
}

// Using arrow function directly
processItems(products, (product, index) =>
console.log(`[${index + 1}] ${product.toUpperCase()}`)
);

// Output:
// [1] LAPTOP
// [2] MOUSE
// [3] KEYBOARD
// [4] MONITOR

You can see the output in the screenshot below.

Pass TypeScript Functions as Parameters

Advanced Arrow Function Pattern

Here’s a more sophisticated example with type inference:

const processWithLogger = (
items: string[],
processor: (item: string, index: number) => string
): string[] => {
return items.map((item, idx) => {
console.log(`Processing item ${idx + 1}...`);
return processor(item, idx);
});
};

const userNames = ["alex", "taylor", "jordan"];
const formatted = processWithLogger(
userNames,
(name, index) => `${index + 1}. ${name.charAt(0).toUpperCase()}${name.slice(1)}`
);

console.log(formatted);
// Output: ["1. Alex", "2. Taylor", "3. Jordan"]

Optional Function Parameters

Sometimes you want a function parameter to be optional. Here’s how:

function executeTask(
task: () => void,
logger?: (message: string) => void
): void {
const log = logger ?? ((msg: string) => console.log(msg));

log("Starting task...");
task();
log("Task completed");
}

// With custom logger
executeTask(
() => console.log("Doing work..."),
(msg) => console.log(`[CUSTOM] ${msg}`)
);

// Without logger (uses default)
executeTask(() => console.log("Doing work..."));

Default Function Parameters

You can also provide default implementations:

type Logger = (message: string) => void;

const defaultLogger: Logger = (msg) => console.log(`[LOG] ${msg}`);

function runProcess(
operation: () => void,
logger: Logger = defaultLogger
): void {
logger("Process started");
operation();
logger("Process finished");
}

runProcess(() => console.log("Working..."));
// Uses default logger

Generic Function Parameters

Generics let you write flexible, type-safe functions. This is advanced but incredibly powerful:

function mapArray<T, U>(
array: T[],
transformer: (item: T) => U
): U[] {
return array.map(transformer);
}

// Transform numbers to strings
const numbers = [1, 2, 3, 4, 5];
const numberStrings = mapArray(numbers, (n) => `Number: ${n}`);
console.log(numberStrings);
// Output: ["Number: 1", "Number: 2", "Number: 3", "Number: 4", "Number: 5"]

// Transform strings to objects
const names = ["Chris", "Morgan", "Taylor"];
const userObjects = mapArray(names, (name) => ({
name,
email: `${name.toLowerCase()}@example.com`
}));
console.log(userObjects);
// Output: [
// { name: "Chris", email: "chris@example.com" },
// { name: "Morgan", email: "morgan@example.com" },
// { name: "Taylor", email: "taylor@example.com" }
// ]

The beauty here is TypeScript automatically infers the types. When you pass a number[] and a function that returns a string, it knows the result is string[].

Common Function Parameter Patterns

Here are some common function parameter patterns.

Filter Pattern

type Predicate<T> = (item: T) => boolean;

function filterArray<T>(
array: T[],
predicate: Predicate<T>
): T[] {
return array.filter(predicate);
}

const ages = [15, 22, 18, 30, 16, 45];
const adults = filterArray(ages, (age) => age >= 18);
con