Function types are contravariant in their parameter types; see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more details. Contravariance means the direction of assignability flips; if T
is assignable to U
, then (...u: U) => void
is assignable to (...t: T) => void
and not vice versa. This is necessary for type safety. Picture the direction of data flow: if you want fruit then I can give you an apple, but if you want something that will eat all your fruit I can't give you something that eats only apples.
The function type (xx: number) => void
is equivalent to (...args: [number]) => void
, and you cannot assign that to (...args: unknown[]) => void
. Yes, [number]
is assignable to unknown[]
, but that's not the direction we care about. Your assignment is therefore unsafe. If this worked:
const y: (...args: unknown[]) => unknown =
(xx: number) => xx.toFixed(); // should this be allowed?
Then you'd be able to call y()
with any set of arguments you wanted without a compiler error, but hit a runtime error:
y("x", 32, true); // no compiler error
// error! xx.toFixed is not a function
Widening the input argument list to unknown[]
has the effect of making the function type very narrow, since most functions do not accept all possible argument lists.
So if you really want a type to which any function at all should be assigned, you'd need to narrow the input argument list to a type that cannot accept any inputs, like this:
type SomeFunction = (...args: never) => unknown;
const y: SomeFunction = (xx: number) => xx.toFixed(); // okay
// const y: SomeFunction
That works because SomeFunction
is essentially uncallable (well, it should be; there's an outstanding bug at ms/TS#48840). If I ask you for a function that I'm not going to call, you can safely hand me any function at all. Conversely if you hand me a function and I don't know what arguments it accepts, I had better not try to call it.
So that works, but... it's kind of useless. Once you have y
you can't do anything with it:
y(123); // error, oops doesn't know about its argument types anymore
For the use case in your question I guess that's fine, since you are passing these uncallable functions to non-TypeScript code to something which has no knowledge of or regard for our type safety rules.
Still, for others who might be reading, it might be more useful to verify that a value can be assigned to a type without widening it to that type. So instead of
annotating y
as SomeFunction
, we can use the satisfies
operator to just check it against that type:
const y = ((xx: number) => xx.toFixed()) satisfies SomeFunction;
// const y: (xx: number) => string
That compiles (but would fail if you wrote, say, const y = "oops" satisifies SomeFunction
), and y
is still known to be (xx: number) => string
. So you still can call it:
y(123); // okay
Playground link to code