Why doesn't this work?
const x: unknown[] = ['x', 32, true]; // OK
const y: (...args: unknown[]) => unknown = (xx: number) => {}; // ERROR
// Type '(xx: number) => void' is not assignable to type '(...args: unknown[]) => unknown'.
// Types of parameters 'xx' and 'args' are inpatible.
// Type 'unknown' is not assignable to type 'number'. ts(2322)
My goal is to make sure that y
is any runnable function. I was trying not to use any
.
Hope to improve my understanding of how unknown
works in this case.
Why doesn't this work?
const x: unknown[] = ['x', 32, true]; // OK
const y: (...args: unknown[]) => unknown = (xx: number) => {}; // ERROR
// Type '(xx: number) => void' is not assignable to type '(...args: unknown[]) => unknown'.
// Types of parameters 'xx' and 'args' are inpatible.
// Type 'unknown' is not assignable to type 'number'. ts(2322)
My goal is to make sure that y
is any runnable function. I was trying not to use any
.
Hope to improve my understanding of how unknown
works in this case.
-
1
What you're doing isn't safe. Presumably you would want
const y: (...args: unknown[]) => unknown = (xx: number) => xx.toFixed()
to pile, but theny("x", 32, true)
would be accepted by the piler and subsequently blow up at runtime. What do you plan to do withy
once it exists? That will determine how it should be declared. – jcalz Commented Dec 30, 2022 at 16:09 -
I'm trying to make a definition for a module within the dependency injection library
didi
which isn't very type-safe either. Module declarations are one of the following:['type', FunctionConstructor]
,['factory', FactoryFunction]
,['value', unknown]
. – Sam Chen Commented Dec 30, 2022 at 16:16 -
I used the example above to simplify the reason for the error.
y
should actually return something specific. I left it empty for simplicity. But there's no way I know the function parameters of every factory function I may want to use for injection later on. I use unknown sincedidi
doesn't have the type bindings to give me each factory function's return type when I inject anyway, so I'm essentially casting the injected value's type at the destination. – Sam Chen Commented Dec 30, 2022 at 16:18 -
There is a (mostly) safe top type for functions; it's
(...args: never) => unknown
. It's theunknown
of functions. But as such, it's almost useless to have a value annotated of that type; the piler won't let you call it. This is the general tradeoff with types; the less you specify about a type, the easier it is to produce values of that type and the harder it is to consume values of that type. I wish you'd edit to show a minimal reproducible example of someone usingy
, since that drives the answer. Perhaps you don't want to annotate at all and instead usesatisfies
like this? – jcalz Commented Dec 30, 2022 at 16:27 - 1 If you're not calling the functions in TypeScript then I guess I don't need a minimal reproducible example. I'll write up an answer. – jcalz Commented Dec 30, 2022 at 16:39
2 Answers
Reset to default 7Function 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 piler error, but hit a runtime error:
y("x", 32, true); // no piler error
//