How to Define a TypeScript Function Type Callback That Accepts Any Signature Without Using `any`
Use a generic function type with unknown constraints: type Callback<Args extends readonly unknown[] = unknown[], R = unknown> = (...args: Args) => R;
The microsoft/TypeScript repository demonstrates that when you need a flexible typescript function type callback, you should avoid the universal (...args: any[]) => any pattern. Instead, generic type parameters preserve type safety while allowing any function signature, letting the compiler infer argument and return types at the call site.
Why Universal Function Types Break Type Safety
A naïve approach to accepting "any function" uses the type (...args: any[]) => any. This creates a universal type that disables type checking for both parameters and return values. Once you annotate a callback with this type, the compiler can no longer verify that arguments passed to the function match its expected signature, nor can it infer what the function returns. This silently erases type information and allows invalid operations at runtime.
The Generic Callback Pattern
The robust solution defines a generic callback type using unknown instead of any. This pattern captures the argument list and return type as type parameters, maintaining type awareness without restricting the specific shape of the function.
Basic Generic Implementation
Define the type with two parameters: Args for the tuple of argument types and R for the return type:
type Callback<Args extends readonly unknown[] = unknown[], R = unknown> = (
...args: Args
) => R;
This definition uses unknown with constraints to prevent the type from becoming a catch-all any. The defaults (unknown[] and unknown) allow the type to represent any function when specific parameters are not provided, while concrete instantiations preserve full type information.
Usage in Higher-Order Functions
You can use this pattern in higher-order functions that invoke callbacks safely:
function invoke<T extends unknown[], R>(cb: Callback<T, R>, ...args: T): R {
return cb(...args);
}
// Example: explicit typing
const ok: Callback<[string, number], boolean> = (s, n) => s.length > n;
const result = invoke(ok, "hello", 2); // result is inferred as boolean
The compiler validates that "hello" and 2 match the [string, number] tuple and that result receives the boolean return type.
How TypeScript Implements This Internally
The TypeScript compiler itself distinguishes between internal utility types and public-facing generics. According to the source code in src/compiler/core.ts at line 1934, the repository defines an internal AnyFunction type for compiler use:
// Located at src/compiler/core.ts#L1934
type AnyFunction = (...args: never[]) => void;
This internal type serves as a placeholder for "any callable" within the compiler's implementation details. However, for public APIs and user code, the TypeScript codebase favors explicit generic patterns like the Callback type shown above, which preserve type relationships rather than discarding them.
Complete Code Examples
Inferring Types Automatically
When you wrap a function using generics, TypeScript can infer the specific signature from the supplied callback:
function wrap<T extends unknown[], R>(cb: Callback<T, R>) {
return (...args: T) => cb(...args);
}
const add = (a: number, b: number) => a + b;
const safeAdd = wrap(add); // safeAdd: (a: number, b: number) => number
Mapping Values with Callbacks
Generic callbacks work seamlessly with utility functions that transform data:
function mapCallback<A, B>(cb: Callback<[A], B>, value: A): B {
return cb(value);
}
const toString: Callback<[number], string> = n => n.toString();
const str = mapCallback(toString, 42); // "42"
Comparison with Non-Generic Alternatives
| Type | Safety Level | Use Case |
|---|---|---|
(...args: any[]) => any |
None (universal) | Avoid in public APIs |
(...args: unknown[]) => unknown |
Low | When you only need "callable" verification |
Callback<T, R> |
High | When parameter and return types must be preserved |
Summary
- Avoid
(...args: any[]) => anyfor callback types because it creates a universal type that disables type checking. - Use a generic
Callback<Args, R>definition withunknownconstraints to accept any function signature while preserving type information. - Reference
src/compiler/core.tsline 1934 to see how TypeScript internally uses restricted function types for compiler utilities. - Leverage type inference with higher-order functions like
invoke,mapCallback, orwrapto maintain strict type safety without verbose annotations.
Frequently Asked Questions
What is the difference between (...args: any[]) => any and a generic callback type?
(...args: any[]) => any is a universal type that tells TypeScript to ignore type checking for both arguments and return values. A generic callback type like Callback<T, R> captures the specific argument tuple T and return type R, allowing the compiler to verify that calls match the function signature and to infer the result type correctly.
Why should I avoid using any for function callbacks in TypeScript?
Using any for callbacks disables the compiler's ability to catch mismatched arguments or incorrect return value usage. It also prevents IntelliSense and autocomplete features from working correctly in IDEs. The unknown type with generics provides the same flexibility without sacrificing type safety.
How does TypeScript's own compiler define internal function types?
According to the microsoft/TypeScript source code, the internal AnyFunction type is defined in src/compiler/core.ts at line 1934 as (...args: never[]) => void. This internal type is restricted to compiler-specific use cases, while the codebase encourages public APIs to use more expressive generic patterns that retain type information.
Can I constrain the return type to void for callback functions?
Yes, you can create a specialized version by setting R = void or constraining the generic: type VoidCallback<Args extends unknown[]> = Callback<Args, void>. This ensures the callback does not return a usable value, though TypeScript will still allow functions that return values to be assigned to it (it only checks that you don't use the return value).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s https://instagit.com/install.md