Description
After work on #24423, and discussion at #24593, we've come to the conclusion that we won't be adding a new --strictAny
flag. Here's the rationale behind it.
Background
Recently a few of us met with the Hack team and discussed their proposed dynamic
type which acts like any
behaviorally but is as restrictive as our new unknown
type with respect to assignability. In other words, you can dot off of dynamic
, you can call/construct dynamic
, etc., but you can't assign a dynamic
to anything other than another dynamic
type. This gives some of the ease-of-use of any
, but ensures that that usage is strictly scoped, and escape has to be intentional.
Goal
We started with a few goals: to bring our users
- the functionality of
dynamic
(i.e. a scoped version ofany
) - without introducing another concept (i.e. a new
dynamic
type) - and something else that we thought more about after the fact (keep reading)
Rather than introduce a new concept, we opted to introduce a more restrictive mode for any
that treated it as this dynamic
type (i.e. --strictAny
). This has the added benefit that users decide how strict any
is rather than declaration authors choosing whether to use any
or unknown
or {}
etc., and I believe that this became the third goal over time.
Problems
We experimented with --strictAny
as a new flag in #24423 and the short story is that while the mode felt like it was catching a lot of questionable code, we started seeing a lot of code that quickly became tough to reason about.
Contravariance for any
-ful signatures
One of the first things that popped up was that the type (...args: any[]) => any
was no longer as flexible as before. Since the main change in --strictAny
is to remove one direction of assignability of any
with everything else, running code with --strictFunctionTypes
meant that almost no functions were assignable to (...args: any[]) => any
anymore.
Aside: why?
Think about assigning a value of type
(x: number) => any
to a binding of type(...args: any[]) => any
. Because parameters are compared in the reverse direction in--strictFunctionTypes
, we effectively try to see ifany
is assignable tonumber
, and it wouldn't be under--strictAny
!
One solution was to have users use a signature like (...args: never[]) => any
instead (which has issues as we'll see). Instead, we special-cased this construct, though this didn't cover less-contrived signatures like (x: any, xs: any[]) => any
.
Users need to know never
We've always told users that any
is your escape hatch in TypeScript. Under --strictAny
, you can still access any member on a value of type any
- but you've lost the ability to say "just trust that I know what I'm doing" with it.
As mentioned on #24423 (comment)
My feeling is that this behavior is what I want from
any
around 50% of the time. The other 50% of the time I want the current behavior, which is when I want to say "damn it, this type is going to be a huge pain, leave me alone". For exampledeclare function foo(x: Some & Very & Long<Type, Annotation | Have<Fun, Writing, This>>); // Now an error! foo({ /*...*/ } as any);The above call to
foo
will no longer work, sinceany
is no longer assignable toSome & Very & Long<Type, Annotation | Have<Fun, Writing, This>>
.The workaround is actually very simple - use
never
instead ofany
:declare function foo(x: Some & Very & Long<Type, Annotation | Have<Fun, Writing, This>>); // Always worked! foo({ /*...*/ } as never);But now you really have to know how both of these work, whereas I suspect most users have never been aware of
never
.
never
, while often very useful, is something users have rarely had to think about. Now, it becomes the primary "just trust me" mechanism for type assertions, and that seems undesirable.
Places the compiler uses any
for simplification
Today, the compiler "cheats" in a few places, substituting in any
when diving into checks that may be very expensive. For example, when comparing signatures from a source type to a target type, if either side has more than one signature, all generic signatures will have their type parameters erased with any
(see getErasedSignature
in checker.ts
).
But this indicates an assumption that we the language creators made about the laxness of any
. And under --strictAny
, code like the following fails because those assumptions have been invalidated.
declare var x: {
<T>(a: T): T;
<T>(a: T): T;
}
declare var y: (a: number) => number;
y = x;
// ~~~~~ Error, even though this is okay!
x = y;
// ~~~~~ Uhh... but this one is definitely wrong and *doesn't* error.
So it seems questionable that users could expect this behavior change when the language itself couldn't.
Existing code and .d.ts
files
Both lib.d.ts
and DefinitelyTyped had breaks due to this. This hasn't always been a problem with other strictness flags, but it's something we can't ignore.
Didn't prove its worth for the cost
While I hate to be blunt, this is kind of the reality. The feature is useful, but maybe not quite enough given how it works today. I think @RyanCavanaugh summarized this best at #24711 (comment)
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 Array
that 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
any
s tend to be very successful at having that not happen, and are only usingany
where they really don't get value from the type system.
Is there any hope?
Alternatives we discussed include:
- a new
dynamic
type- but we don't want another type
- a mode where
unknown
becomes as lax asany
in--strictAny
(or even making that the default)- but now you're giving people a confusing top-type
We don't feel like these alternatives are significantly better (or necessarily as good). For that reason, we don't think we're going to pursue --strictAny
in the near future.