4

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.

7
  • 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. 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]. 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 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. Commented Dec 30, 2022 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 minimal reproducible example of someone using y, since that drives the answer. Perhaps you don't want to annotate at all and instead use satisfies like this? 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. Commented Dec 30, 2022 at 16:39

2 Answers 2

9

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

Sign up to request clarification or add additional context in comments.

Comments

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.

1 Comment

here is an example: Type-safe data fetching of checking an unknown's runtime value.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.