1

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 incompatible.
//    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.

Sam Chen
  • 137
  • 9
  • 1
    What you're doing isn't safe. Presumably you would want `const y: (...args: unknown[]) => unknown = (xx: number) => xx.toFixed()` to compile, but then `y("x", 32, true)` would be accepted by the compiler and subsequently blow up at runtime. What do you plan to do with `y` once it exists? That will determine how it should be declared. – jcalz Dec 30 '22 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 Dec 30 '22 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 since `didi` 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 Dec 30 '22 at 16:18
  • There is a (mostly) safe top type for functions; it's `(...args: never) => unknown`. It's the `unknown` of functions. But as such, it's almost useless to have a value annotated of that type; the compiler 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 [mre] of someone *using* `y`, since that drives the answer. Perhaps you don't want to annotate at all and instead use `satisfies` like [this](//tsplay.dev/WzaXem)? – jcalz Dec 30 '22 at 16:27
  • Okay. I can edit it and make something that uses `didi`. But that will take some time. `satisfies` actually does exactly what I wanted. And your example with `(...args:never)` also works since `didi` calls these functions and it's not using typescript--seems like cheating. Curious why these work but not `unknown` as an annotation. – Sam Chen Dec 30 '22 at 16:33
  • 1
    If you're not calling the functions in TypeScript then I guess I don't need a [mre]. I'll write up an answer. – jcalz Dec 30 '22 at 16:39
  • Must make a correction to what I said about `didi` for future readers: Turns out one of the typedefs it has `ServiceDeclaration` is exactly what I wanted to make. So I didn't have to make my own. This has been very educational, nevertheless. – Sam Chen Dec 30 '22 at 17:23

2 Answers2

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

Why doesn't this work?

The following is an error:

const x: unknown[] = ['x', 32, true]; // OK
const y: (...args: unknown[]) => unknown = (xx: number) => {}; // ERROR

Because of the same reason why this is an error:

const x: unknown = 123; // OK 
const y: number = x; // ERROR: cannot assign unknown to number 

Reason Simplified: You cannot assign unknown to anything else without first checking its runtime value.

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
basarat
  • 261,912
  • 58
  • 460
  • 511