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.

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.

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.

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.

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
| Aspect | JavaScript | TypeScript |
|---|---|---|
| Type Safety | None—errors at runtime | Compile-time type checking |
| IDE Support | Basic autocomplete | Full IntelliSense with parameter hints |
| Refactoring | Manual, error-prone | Automated rename and type updates |
| Documentation | External JSDoc comments | Self-documenting with type signatures |
| Callback Errors | Runtime crashes | Caught before code runs |
| Developer Experience | Guess parameter types | See 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:
- Always type your function parameters; it prevents 80% of callback-related bugs
- Use type aliases for any signature you use more than twice
- Keep optional parameters at the end of the parameter list
- For async callbacks, use Promise types, not void
- Generic function parameters are worth learning; they unlock incredible flexibility
- Error-handling callbacks should be separate from success callbacks
- 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:
- How to Use for…of Loops in TypeScript?
- TypeScript forEach Loop with Index
- How to Break Out of a forEach Loop in TypeScript?
- How to Break Out of Loops in TypeScript?

Bijay Kumar is an experienced Python and AI professional who enjoys helping developers learn modern technologies through practical tutorials and examples. His expertise includes Python development, Machine Learning, Artificial Intelligence, automation, and data analysis using libraries like Pandas, NumPy, TensorFlow, Matplotlib, SciPy, and Scikit-Learn. At PythonGuides.com, he shares in-depth guides designed for both beginners and experienced developers. More about us.