When it comes to function types, they exhibit contravariant behavior in their parameter types. To understand this concept better, refer to the article on Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript. In simple terms, contravariance means that if a type T
can be assigned to type U
, then the function (...u: U) => void
is assignable to (...t: T) => void
, and not the other way around. This direction of assignability is crucial for maintaining type safety. Think of data flow: I can give you an apple if you want fruit, but I cannot hand you something that eats only apples when you need something to consume all your fruit.
The function type (xx: number) => void
is effectively equivalent to (...args: [number]) => void
, and it cannot be directly assigned to (...args: unknown[]) => void
. Even though [number]
can be assigned to
unknown[]</code, our concern here is not about that specific direction. Hence, trying such an assignment would lead to unsafe behavior. For instance:</p>
<pre><code>const y: (...args: unknown[]) => unknown =
(xx: number) => xx.toFixed(); // Should this actually work?
If the above code was allowed, calling y()
with any arguments would compile successfully without errors, but result in a runtime error:
y("x", 32, true); // No compiler error
// 💥 Error! xx.toFixed is not a function
By widening the input argument list to unknown[]
, we unintentionally make the function type too narrow since most functions do not cater to every possible argument combination.
If you want a type that accepts any function at all, you'd have to limit the input argument list to a type like never
which does not accept any inputs:
type SomeFunction = (...args: never) => unknown;
const y: SomeFunction = (xx: number) => xx.toFixed(); // This works fine
// const y: SomeFunction
This approach works because SomeFunction
becomes practically uncallable. If someone asks for a function they won't call, any function will suffice. Conversely, if a function's expected arguments are unknown, it should not be called unknowingly.
While this method accomplishes the task, it doesn't offer much functionality beyond the initial assignment:
y(123); // Results in an error as argument types are no longer known
In your use case, where these "uncallable" functions might be passed to environments unaware of TypeScript's type rules, this limitation may not pose a problem.
For others interested in verifying assignments without broadening them, using the satisfies operator is a valuable technique. Instead of explicitly annotating y
as
SomeFunction</code, consider checking it against that type via <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator" rel="noreferrer">the satisfies operator</a>:</p>
<pre><code>const y = ((xx: number) => xx.toFixed()) satisfies SomeFunction;
// const y: (xx: number) => string
This method compiles successfully (but fails with incorrect usage), confirming that y
remains as (xx: number) => string
. Therefore, you can continue calling it as usual:
y(123); // Works fine
Playground link to code