How to stop using any for callbacks and start living: a complete guide to typing handler functions in TypeScript
\n\nHave you ever written something like callback: any in method parameters? If yes — welcome to the club. If not — you are either a genius or just starting your journey with TypeScript. Just kidding. But there is truth in every joke: according to Stack Overflow, the question "How to define type for a function callback (as any function type, not universal any)" gathers thousands of views. And that's no coincidence.
The problem is that any is like a black hole: it sucks all the type safety out of your code. You lose autocompletion, argument checking, and the ability to catch bugs at compile time. But the most frustrating thing is that any as a callback type kills the very idea of TypeScript. Let's figure out how to properly type a callback function in a method parameter so that the code is both safe and elegant.
Why is Function no better than any?
\n\nA beginner's first instinct is to use the built-in Function type. After all, it's the "any function" type, so it should work. But no. Function is just as "garbage" a type as any. It provides no information about the signature: how many parameters the function takes, what it returns, what types the arguments are. You're just telling the compiler: "Trust me, I know what I'm doing." Spoiler: you usually don't.
// Bad: no type checking\nfunction processData(callback: Function) {\n callback(42, "hello"); // Error? Who knows\n}\n\nWith this approach, you lose all the benefits of static typing. Let's fix that.
\n\nStep 1. Basic callback typing: simple and clear
\n\nThe simplest and most correct way is to declare the function type directly in the parameter. For this, the arrow syntax (parameters) => return_type is used. This is the function type in TypeScript.
// Good: explicitly define the signature\nfunction fetchData(url: string, onSuccess: (data: string) => void): void {\n // ... some logic\n const result = "Data from server";\n onSuccess(result);\n}\n\n// Usage — with autocompletion and checking\nfetchData("/api/users", (data) => {\n console.log(data.toUpperCase()); // data is string, all good\n});\n\nNotice the void in the return type. This means we don't expect any specific value from the callback. If the callback returns something, it will be ignored. If you expect a result, use a specific type.
Step 2. Callback with multiple parameters
\n\nOften callbacks accept more than one argument. For example, an error handler or an event with data. It's just as simple — list all parameters separated by commas.
\n\n// Type for a callback with error and result\nfunction loadUser(\n userId: number,\n callback: (error: Error | null, user: { name: string; age: number } | null) => void\n): void {\n // ... loading logic\n const user = { name: "Anna", age: 30 };\n callback(null, user);\n}\n\n// Usage\nloadUser(42, (err, user) => {\n if (err) {\n console.error("Error:", err.message);\n return;\n }\n if (user) {\n console.log(`Hello, ${user.name}!`);\n }\n});\n\nSee how TypeScript automatically suggests the types for err and user? This is callback function typing in action. No any, no magic — just strict checking.
Step 3. Creating reusable types for callbacks
\n\nIf the same callback type is used in multiple places, don't duplicate it every time. Extract it into a separate type alias or interface. This improves readability and simplifies maintenance.
\n\n// Decla