TypeScript: Functions as Parameters with Strict Typing — Full Breakdown with Examples
\n\nHello, colleague! If you've ever written in JavaScript and passed a function to another function (for example, in setTimeout or Array.map), you're already familiar with the concept of higher-order functions. But when it comes to TypeScript, a logical question arises: "Can I strictly type the function I pass as a parameter?". The answer is yes, and this is one of the most powerful features of the language.
In this article, we'll break down how to declare function types in TypeScript, how to pass them as arguments, and how not to mess up with signatures. You'll learn how to make your code not only safe but also self-documenting. Let's go!
\n\nBy the way, this question is one of the most popular on Stack Overflow (thousands of views). So if you want to understand it once and for all — you're in the right place.
\n\nWhy This Matters: Strict Typing Saves Time
\n\nImagine you're writing a function that takes another function (a callback). Without types, you might accidentally pass a function with the wrong number of arguments or the wrong return value. TypeScript catches such errors at compile time, not at runtime. It's like insurance for your code.
\n\nHere's an example that won't work without types:
\n\n// JavaScript (without types) — error will only appear at runtime\nfunction processData(callback) {\n const result = callback(10, 20);\n console.log(result);\n}\n\n// Oops, passed a function with one parameter\nprocessData((x) => x * 2); // NaN — because the second parameter is undefined\n\nWith TypeScript, we can explicitly specify what signature the callback expects:
\n\n// TypeScript — error at compile time\nfunction processData(callback: (a: number, b: number) => number): void {\n const result = callback(10, 20);\n console.log(result);\n}\n\n// ✅ Correct\nprocessData((x, y) => x + y); // 30\n\n// ❌ Error: Type '(x: number) => number' is not assignable to type '(a: number, b: number) => number'\n// processData((x) => x * 2);\n\nCool, right? Now let's break down all the ways to declare such types.
\n\nMethod 1: Inline Signature (Simplest)
\n\nThe most straightforward way is to specify the function signature directly in the parameters. This is convenient when the function parameter is used only once.
\n\n// A function that takes another function with two numbers and returns a string\nfunction formatNumbers(\n a: number,\n b: number,\n formatter: (x: number, y: number) => string\n): string {\n return formatter(a, b);\n}\n\n// Usage\nconst result = formatNumbers(5, 10, (x, y) => `Sum: ${x + y}`);\nconsole.log(result); // "Sum: 15"\n\nPros: fast, clear.
\nCons: if the signature is repeated, you'll have to duplicate code.
Method 2: Type Alias for a Function (Reusable)
\n\nIf you use the same signature in multiple places, it's better to extract it into a type alias. This makes the code cleaner and easier to maintain.
\n\n// Declare the function type\ntype Callback = (error: Error | null, data: string) => void;\n\n// A function that takes a Callback\nfunction fetchData(url: string, callback: Callback): void {\n // Simulating an async request\n setTimeout(() => {\n if (url) {\n callback(null, "Data received");\n } else {\n callback(new Error("URL is empty"), "");\n }\n }, 1000);\n}\n\n// Usage\nfetchData("/api/data", (err, data) => {\n if (err) {\n console.error("Error:", err.message);\n } else {\n console.log("Success:", data);\n }\n});\n\nNotice: we used void as the return type. This means the function may not return