2

I wonder if there is an alternative way to specify the function type "any function" that does not use any (and therefore improves type-safety). To clarify, I am searching for an alternative for the following type:

type AnyFunctionAsUsual = (...args: any[]) => any;

I was able to replace the return value with unknown, but there is still an any in the following definition:

type AnyFunctionReturningUnknown = (...args: any[]) => unknown;

As far as I know, the types AnyFunctionAsUsual and AnyFunctionReturningUnknown may take the same values. And I also think that the latter is sufficiently type-safe (because the any for the parameters does not disable any type checking, as far as I am concerned).

But eslint does not like it as it still violates the rule no-explicit-any, which is included in plugin:@typescript-eslint/recommended.

Is there a way to define a type that takes the same values as the two types above and also makes the linter happy? (Where disabling the linter or the rule is not the same as making the linter happy.)

Islingre
  • 2,030
  • 6
  • 18
  • The "top type" for Functions is `(...args: never) => unknown`; it is essentially uncallable (or will be once TS5.0 comes out). `(...args: any[]) => unknown` is indeed unsafe, since it lets you call it no matter what. See [this playground link](https://tsplay.dev/Nr8r5W). Does that fully address your question? If so I'll write up an answer; if not, what am I missing? – jcalz Mar 03 '23 at 17:16
  • 1
    Re-reading... I'm a bit confused; do you care about type safety or quieting the linter? They are different; you could maybe use `Function` instead of `(...args: any[]) => any` and avoid the `no-explicit-any` rule (but maybe run afoul of some other rule)? But neither one is *safe* compared to `(...args: never) => unknown`. How should we proceed? – jcalz Mar 03 '23 at 17:20
  • @jcalz Yeah, I guess ```(...args: never) => unknown``` is what I was searching for. I tried ```(...args: never[]) => unknown``` which did not work. Perhaps you can give information about why the one works but the other does not, when you write an answer. ```Function``` is banned by the rule ```ban-types```. I guess this is because of it also accepting classes (feel free to write a paragraph about it if you do not think it is too far away from the question..). I know there are also "new Functions" (```new (...) => ...```) but I do not understand them yet. Are this only constructors? – Islingre Mar 05 '23 at 17:29
  • 1
    Those are constructors, yes. I’m probably not going to digress about `Function` or even eslint in my answer when I write it up; I think ban-types warns on `Function` because it’s unsafe, not because of classes. Anyway I’ll write an answer when I get a chance. – jcalz Mar 05 '23 at 17:31

1 Answers1

1

You want SomeFunction to be a top type for functions, where if you have a function f you can assign it to a variable of type SomeFunction, and if you have a non-function x then you can't assign it to SomeFunction. Sort of the unknown type for functions. (You can't use unknown because it accepts non-functions.)


Your (...args: any) => unknown or (...args: any[]) => unknown types meet these criteria, but they are not type safe because they use the unsafe any "type". In a safe type system, there is a tradeoff where values of top types are easy to supply but hard to consume. If I want a value of type unknown you can give me anything you want. But once I have it, I don't know what to do with it without inspecting it further. If I want a value of type SomeFunction, you can give me any function you want. So once I have it, I could do anything that works for an arbitrary function, such as inspecting its length property or... uh... well, there's not much I can do with it. I can't call it, because I don't know what arguments it would accept. So consuming a SomeFunction will be difficult, or at least tricky. The (...args: any) => unknown type allows you to call it however you want, even if this would immediately lead to a runtime error:

type SomeFunction = (...args: any) => unknown;
const f: SomeFunction = (x: string) => x.toUpperCase();
f(); // accepted erroneously, runtime error!
f(1, 2, 3); // accepted erroneously, runtime error!

That's the reason why you shouldn't use any there.


Assuming you want a safe version of SomeFunction, then we should make sure that the argument list isn't given an "anything-goes" type. Indeed, it should be the opposite. You should be completely unable to call it. That's because function types are contravariant in their parameter types (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript and a description of the --strictFunctionTypes compiler option for more information). In TypeScript, that means the top function type should have a bottom type for its parameter list. And in TypeScript, the bottom type is the never type:

type SomeFunction = (...args: never) => unknown;
const f: SomeFunction = (x: string) => x.toUpperCase();
f(); // error in TS5.0 and above
f(1, 2, 3); // error

This ability for (...args: never) => unknown to act as a function top type was implemented mostly in microsoft/TypeScript#35438 and completed in microsoft/TypeScript#52387, which will be released with TypeScript 5.0. Before then it's possible to call f() above without error (even though it is not safe), but other calls are rejected.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for your answer! Do you know why ```(...args: never[]) => unknown``` does not work? I would ```...args``` expect to always be an array type. – Islingre Mar 06 '23 at 14:09
  • 1
    `[]` is of type `never[]` but not of type `never`, so with `(...args: never[])=>unknown` you *should* be able to call it with zero arguments. But with `(...args: never)=>unknown` you *should not*. Note that `never` is a subtype of every type, so `never` is trivially an array type (in the sense that `never extends any[]` is true). I think you could actually use `(...args: never[]) => unknown` as a top type but there's a safety hole there – jcalz Mar 06 '23 at 14:14
  • Thanks. It seems with the nightly version, ```never[]``` is possible, while it is not with 4.9. You can check with the following link, which contains my solution for the Type Challenge 17 (where I encountered this question): [Playground Link](https://www.typescriptlang.org/play?ts=4.9.5#code/C4TwDgpgBAYgrgOwMYB4CCAnA5gZyhAD2AgQBM8BDBEAbQF0oBeKBCANwgwBooAlCYHAwImURAGsEAewDuCAHyiAFADo1FbDgBcUTLgCUTRf0HCA3AFgAUNdCQoAYSEYQKGPiIlysREkXMABQ0KAFsBThw3eRoAIgAbEixgAAsYhkJiMjwABmsAflhrHSCMUPCMSJho+MSUtI9M7wBGfMKrYuCw4gqohq88GgBLBAAzTlhBiuAeNRVhsYw+CBxgOlalDSwdGEmVw0ZFJwwXFFV1TR1+PaMl0wQAFXAIKPkilnZOMyA) – Islingre Mar 06 '23 at 14:50