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);
console.log(adults); // [22, 18, 30, 45]

Reduce Pattern

type Reducer<T, U> = (accumulator: U, current: T) => U;

function reduceArray<T, U>(
array: T[],
reducer: Reducer<T, U>,
initialValue: U
): U {
return array.reduce(reducer, initialValue);
}

const prices = [10.99, 25.50, 8.75, 15.25];
const total = reduceArray(
prices,
(sum, price) => sum + price,
0
);
console.log(`Total: $${total.toFixed(2)}`); // Total: $60.49

Comparison Pattern

type Comparator<T> = (a: T, b: T) => number;

function sortArray<T>(
array: T[],
comparator: Comparator<T>
): T[] {
return [...array].sort(comparator);
}

const employees = [
{ name: "Brandon", salary: 75000 },
{ name: "Ashley", salary: 92000 },
{ name: "Kevin", salary: 68000 }
];

const sortedBySalary = sortArray(
employees,
(a, b) => b.salary - a.salary
);

console.log(sortedBySalary);
// Output: [
// { name: "Ashley", salary: 92000 },
// { name: "Brandon", salary: 75000 },
// { name: "Kevin", salary: 68000 }
// ]

Common Mistakes and How to Fix Them

Let me walk you through the mistakes I’ve seen developers make (and some I’ve made myself) when passing functions as parameters in TypeScript.

Mistake 1: Missing Return Type

The Problem: When you don’t specify the return type for the function parameter, TypeScript can’t catch mismatches between what the function returns and what you’re trying to do with that return value. This leads to runtime errors that should have been caught during development.

// Wrong - no return type specified
function processData(
callback: (data: string) => void,
data: string
) {
return callback(data); // Error: callback returns void, not string
}

// Correct - return type matches what callback actually returns
function processData(
callback: (data: string) => string,
data: string
): string {
return callback(data);
}

Why this matters: I once spent 20 minutes debugging why my data transformation function wasn’t working, only to realize the callback was typed to return void when I needed it to return a string. The code compiled fine, but the result was always undefined.

Mistake 2: Optional Parameters in Wrong Position

The Problem: TypeScript requires optional parameters to come after required parameters. If you put an optional parameter before a required one, the compiler throws an error because there’s no way to skip the optional parameter when calling the function.

// Wrong - optional parameter before required
function executeTask(
logger?: (msg: string) => void,
task: () => void // Error: Required parameter after optional
): void {
// ...
}

// Correct - required parameters first, optional last
function executeTask(
task: () => void,
logger?: (msg: string) => void
): void {
const log = logger ?? ((msg) => console.log(msg));
log("Starting task");
task();
log("Task complete");
}

Why this matters: This is a common mistake when refactoring code. You add an optional logger or callback, but if you put it in the wrong position, your entire function signature breaks and you’ll get compilation errors everywhere the function is used.

Mistake 3: Not Handling Async Functions

The Problem: Asynchronous functions return Promises, not direct values. If you type a function parameter as returning void or a plain type when it’s actually async, you’ll get unexpected behavior—your code will continue executing before the async operation finishes, and you won’t be able to properly await or handle errors.

// Wrong - treating async function as synchronous
function runTask(task: () => void): void {
task();
console.log("Done"); // This runs BEFORE async task finishes!
}

// Correct - properly handle async functions with Promise return type
async function runTask(task: () => Promise<void>): Promise<void> {
await task();
console.log("Done"); // Now this runs AFTER task completes
}

// Usage
runTask(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Task complete");
});
// Output order: "Task complete" then "Done"

Why this matters: In SharePoint Framework development, almost every API call is asynchronous. If you don’t type your callbacks correctly, you’ll have race conditions where UI updates before data loads, or error handlers that never catch failures.

Mistake 4: Forget Context/Binding Issues

The Problem: When you pass a class method as a callback, the this context can get lost. JavaScript doesn’t automatically bind this to the class instance when you pass methods around, leading to undefined errors when the method tries to access instance properties or other methods.

class DataManager {
private data: string[] = ["item1", "item2", "item3"];

public processItems(processor: (items: string[]) => void): void {
processor(this.data); // 'this' might be undefined inside processor!
}

public logItems(): void {
console.log(this.data); // Error if 'this' is lost
}
}

// Problem example
const manager = new DataManager();
const processFunc = manager.logItems; // Lost 'this' binding!
manager.processItems(processFunc); // Runtime error: Cannot read property 'data' of undefined

// Solution 1: Arrow function
manager.processItems(() => manager.logItems());

// Solution 2: Bind explicitly
manager.processItems(manager.logItems.bind(manager));

// Solution 3: Bind in constructor (best practice)
class DataManager {
private data: string[] = ["item1", "item2", "item3"];

constructor() {
this.logItems = this.logItems.bind(this);
}

public processItems(processor: (items: string[]) => void): void {
processor(this.data);
}

public logItems(): void {
console.log(this.data);
}
}

Why this matters: This is the number one bug I see in React components and SharePoint web parts. You pass a class method as an event handler, and suddenly this is undefined when the event fires. Always bind methods that you plan to pass as callbacks.

Mistake 5: Overly Complex Type Signatures

The Problem: When function parameter types get too long and complex, the code becomes hard to read and maintain. Future developers (including you in 3 months) won’t understand what the function expects without studying it carefully. Complex inline types also make refactoring difficult.

// Hard to read and maintain
function execute(
fn: (a: string, b: number, c: boolean, d: { x: number, y: string, z: boolean[] }) => { result: string, status: number, errors: string[], metadata: { timestamp: Date, userId: number } }
): void {
// Good luck understanding what this does at a glance!
}

// Better - use type aliases for clarity
type InputData = {
x: number;
y: string;
z: boolean[]
};

type OutputData = {
result: string;
status: number;
errors: string[];
metadata: {
timestamp: Date;
userId: number
};
};

type ProcessorFunction = (
a: string,
b: number,
c: boolean,
d: InputData
) => OutputData;

function execute(fn: ProcessorFunction): void {
// Much clearer what this function expects
}

Why this matters: I’ve worked on projects where developers avoided using certain utility functions because the type signatures were so intimidating. Breaking complex types into named aliases makes your code self-documenting and encourages reuse. It also makes it easier to update types when requirements change; you only modify the type alias instead of hunting down every place the function signature appears.

Performance Considerations

When working with function parameters, keep these performance tips in mind:

Tip 1: Avoid Creating Functions in Loops

// Inefficient - creates new function each iteration
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(n => {
processData((data) => console.log(data + n), "test");
});

// Better - define function outside loop
const processor = (data: string, multiplier: number) =>
console.log(data.repeat(multiplier));

numbers.forEach(n => {
processData((data) => processor(data, n), "test");
});

Tip 2: Use Stable References for Event Handlers

In React or SharePoint web parts:

// Inefficient - new function on every render
class MyWebPart {
public render(): void {
this.attachEvent(() => this.handleClick()); // New function each time!
}

private handleClick(): void {
console.log("Clicked");
}
}

// Better - bind once
class MyWebPart {
constructor() {
this.handleClick = this.handleClick.bind(this);
}

public render(): void {
this.attachEvent(this.handleClick); // Same reference
}

private handleClick(): void {
console.log("Clicked");
}
}

Function Overloads with Parameters

For advanced scenarios, you can use function overloads:

function processData(data: string[], processor: (item: string) => string): string[];
function processData(data: number[], processor: (item: number) => number): number[];
function processData(data: any[], processor: (item: any) => any): any[] {
return data.map(processor);
}

const words = ["hello", "world"];
const upper = processData(words, (w) => w.toUpperCase());
// Type: string[]

const nums = [1, 2, 3];
const doubled = processData(nums, (n) => n * 2);
// Type: number[]

When to Use Each Pattern

Here’s my decision tree for choosing the right approach:

Use inline function types when:

  • You only use the function signature once
  • The signature is simple (1-2 parameters)
  • You’re writing quick utility code

Use type aliases when:

  • You reuse the same function signature multiple times
  • You want to give a semantic name to the function type
  • You’re building a library or shared component

Use interfaces when:

  • The function type is part of a larger contract
  • You need to extend or combine multiple function signatures
  • You’re defining public APIs

Use generics when:

  • The function works with multiple types
  • You want type inference to work automatically
  • You’re building reusable utilities

TypeScript vs JavaScript: Quick Comparison

AspectJavaScriptTypeScript
Type SafetyNone—errors at runtimeCompile-time type checking
IDE SupportBasic autocompleteFull IntelliSense with parameter hints
RefactoringManual, error-proneAutomated rename and type updates
DocumentationExternal JSDoc commentsSelf-documenting with type signatures
Callback ErrorsRuntime crashesCaught before code runs
Developer ExperienceGuess parameter typesSee the exact signature in the editor

Key Takeaways

After working with TypeScript function parameters in dozens of enterprise projects, here’s what I’ve learned:

  1. Always type your function parameters; it prevents 80% of callback-related bugs
  2. Use type aliases for any signature you use more than twice
  3. Keep optional parameters at the end of the parameter list
  4. For async callbacks, use Promise types, not void
  5. Generic function parameters are worth learning; they unlock incredible flexibility
  6. Error-handling callbacks should be separate from success callbacks
  7. Test your function signatures with different input types to verify type safety

The patterns I’ve shown here form the foundation of modern TypeScript development. Whether you’re building React components, SharePoint web parts, Power Platform solutions, or Node.js APIs, you’ll use these techniques constantly.

Start with the basic examples, then gradually work your way up to generics and advanced patterns. Before you know it, passing functions as parameters will feel completely natural.

You may also like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.