I'm very sorry if this is not the place for this, but since #24593 is closed I am opening a new, more specific issue.
This is all related to #24423 (--strictAny).
In #24593 @DanielRosenwasser said:
- There are some issues with signatures like (x: any, y: any, ...zs: any[]) => any.
- Now any is subject to contravariant checking.
- Again, can tell users to use never instead of any.
For me this is a major concern since the pattern (...args: any[]) => any is now the best way to represent a generic function type.
I suppose this can be a _generic type-safe_ function (in the type-safe sense that neither arguments nor return can be used freely without an assertion of some kind):
~ts
(...args: never[]) => unknown
~
Assuming an unknown type as described here #24439. Again as mentioned, never seems very counterintuitive in this scenarios, the same for
~ts
foo({ /.../ } as never)
~
Semantically, that seems you telling _"don't worry, type-checker, that value can never occur"_. I believe the weird part is the name never and not the semantics of never itself.
So I guess my questions are:
With the --strictAny flag enbled, which is the preferred idiom to any in a contravariant position? Will be never a more prominent type and suggested by the TypeScript team? Maybe there are plans to support a different pattern for this (should variadic types help here?)?
Thank you very much in advance.
FWIW you can always comment in closed issues; we do read/reply to those, especially design meeting notes.
The PR notes mention special-casing ...args: any[], which I think is what we'd have to do.
Usually for never, I explain it as "the type of a value that is never observed", which is true of e.g. the return type of functions that always throw, but the explanation holds if you slightly reinterpret "observed" to mean "looked at" - you can have a value of type never so long as you never actually look directly at it.
For what it's worth, I don't think that --strictAny is a direction that we're going to be taking given the sorts of hoops it often forces users to jump through. Details to come.
@RyanCavanaugh
FWIW you can always comment in closed issues; we do read/reply to those, especially design meeting notes.
Ok, thank you. I'll take note of that, I'm sorry :blush:
The PR notes mention special-casing ...args: any[], which I think is what we'd have to do.
Yes, you're right. But since there was suggested the possibility to remove that exception and encourage to use never instead I asked about those.
Usually for never, I explain it as "the type of a value that is never observed", which is true of e.g. the return type of functions that always throw, but the explanation holds if you slightly reinterpret "observed" to mean "looked at" - you can have a value of type never so long as you never actually look directly at it.
I like that interpretation of never. I have to admit the code still _looks_ a little _strange_, but I think it's just a thing of get used to it. Was never planned for this kind of uses?
@DanielRosenwasser
For what it's worth, I don't think that --strictAny is a direction that we're going to be taking given the sorts of hoops it often forces users to jump through. Details to come.
Oh, well, I'll be expecting those details then.
Was the main issue the proliferation of any's in the libraries and type definitions out there?
I think a lot of the conversation in the design meeting notes actually elaborates on it, but very offhandedly, I'd sum it up as
any (even in the compiler!)never.In addition to the above, I think the results of turning on the flag inside the compiler were informative. Our general policy in our codebase is never to use any to "shut up" the type system, we generally write very explicit types, and we don't have any community-written .d.ts files mixing in "external" anys.
Turning the flag on found some assumptions in the tsconfig parser that no one has really noticed (i.e. it looks like you will crash the compiler if the top-level tsconfig content is the literal string "null"), and a bunch of what I would consider noise. There was one instance of new Array that we didn't realize was making an any[]. Every other strict flag we've turned on has found at least one real bug (or at least a "true unsoundness" where something only happened to work) with reasonably minimal noise.
I would honestly hate for this flag to be turned on automatically in a codebase I was maintaining - I just wouldn't expect it to yield any value; teams that are "careful" about not introducing anys tend to be very successful at having that not happen, and are only using any where they really don't get value from the type system.
@InExtremaRes Sorry for the silly question: when you say that (...args: any[]) => any is the best way to represent a generic function, do mean generic in the sense that you can call it with any input and the output can be used in anyway?
I know you define type-safe when you say _generic type-safe_, but I'm not sure what you mean by generic. I feel this is obvious to others, I'm just not really sure I understand all the issues with any in contravariant positions.
@jack-williams
[...] do mean generic in the sense that you can call it with any input and the output can be used in anyway?
Yes, basically. Maybe you are trying to constraint a generic to be an _any function_ type, just like the ReturnType<T> type.
Maybe you need to pass a _printf_-like function (don't know why):
~ts
function setLogger(prinftLike: (m: string, ...args: any[]) => void) { /.../ }
~
Perhaps you have a curry function that currifies the first argument; without variadic types this can be:
~ts
function curry(fn: (a1: U, ...args: any[]) => R, a: U) {
return (...b: any[]) => fn(a, ...b);
}
~
Or just any time you need a function of any kind, even if you are not going to call it yourself. I am not saying these are good patterns or that it is the best way to do it, just giving examples of what I mean.
I'm just not really sure I understand all the issues with any in contravariant positions.
As you probably known, with --strictFunctionTypes flag the arguments positions are checked contravariantly:
~ts
declare let f1: (arg: T1) => void;
declare let f2: (arg: T2) => void;
f1 = f2;
~
So for that last assignment to work T1 must be assignable to T2.
If any cannot be assigned to anything and without an exception as noted in the PR, then this would happen:
~~~ts
declare function bar(a1: string, b2: number): void;
function foo1(fn: (...args: any[]) => void) { /.../ }
function foo2(fn: (...args: {}[]) => void) { /.../ }
foo1(bar); // :)
foo2(bar) // type '{}' not assignable to type 'string' :C
~~~
I am using the empty object type {} since is a type that cannot be assigned to anything but anything can be assigned to it, similar to how any could work in this situations with --strictAny enabled.
I am sorry if I'm over explaining here or I'm not answering your question at all.
@InExtremaRes
I am sorry if I'm over explaining here or I'm not answering your question at all.
Everything is very clear, thank you! I appreciate the very detailed explanation. I now understand the problem. Where I was getting lost is that in your example:
declare function bar(a1: string, b2: number): void;
function foo1(fn: (...args: any[]) => void) { /*...*/ }
function foo2(fn: (...args: {}[]) => void) { /*...*/ }
foo1(bar); // :)
foo2(bar) // type '{}' not assignable to type 'string' :C
I think of the any as being in a covariant position rather than contravariant; the any is contravariant in fn, which is itself contravariant in foo1.
@DanielRosenwasser should we remove it from the 3.0 roadmap for now? Or are we still going to ship it there?
@mhegazy removed it from the roadmap. I've elaborated on the decision here: https://github.com/Microsoft/TypeScript/issues/24737
Most helpful comment
In addition to the above, I think the results of turning on the flag inside the compiler were informative. Our general policy in our codebase is never to use
anyto "shut up" the type system, we generally write very explicit types, and we don't have any community-written .d.ts files mixing in "external"anys.Turning the flag on found some assumptions in the tsconfig parser that no one has really noticed (i.e. it looks like you will crash the compiler if the top-level tsconfig content is the literal string
"null"), and a bunch of what I would consider noise. There was one instance ofnew Arraythat we didn't realize was making anany[]. Every other strict flag we've turned on has found at least one real bug (or at least a "true unsoundness" where something only happened to work) with reasonably minimal noise.I would honestly hate for this flag to be turned on automatically in a codebase I was maintaining - I just wouldn't expect it to yield any value; teams that are "careful" about not introducing
anys tend to be very successful at having that not happen, and are only usinganywhere they really don't get value from the type system.