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.
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.
We started with a few goals: to bring our users
dynamic
(i.e. a scoped version of any
)dynamic
type)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.
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.
any
-ful signaturesOne 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
.
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 https://github.com/Microsoft/TypeScript/pull/24423#issuecomment-393753698
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.
any
for simplificationToday, 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.
.d.ts
filesBoth 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.
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 https://github.com/Microsoft/TypeScript/issues/24711#issuecomment-394934222
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.
Alternatives we discussed include:
dynamic
typeunknown
becomes as lax as any
in --strictAny
(or even making that the default)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.
I would add that, today, expressions of type never
really only occur in places where the type system has determined that the value truly cannot be observed unless some constraint of the type system was violated. The upshot of that is that we have never had to really wrestle with what it means to have never
appear in a property position... but if asserting to never
becomes the commonplace way to get out of a type error, then it's going to be in input positions, and if it's in input positions then it'll eventually be in output positions, and once you have a never
in an output position you have to be real careful - foo<T>(x: T): T | string
produces any
if invoked foo(null as any)
but produces string
if invoked foo(null as never)
, and the latter is even more unsound than the any
in the first place because it appears to provide a constraint that is definitely not guaranteed to be true.
Throwing ourselves into a world where never
is the default cast is a) confusing as hell and b) just adds epicycles like "Add a --noImplicitNever
switch to warn me when never
values creep in to input positions unexpectedly".
It makes no sense. strictAny must be switched on.
Of course there will be problems with switching on but you should rather consider how to do it relatively smoothly.
We hadn't considered thinking about how to work through problems 馃
Of course, a great and good piece of work has already been done. But the good typing system should not allow for such strange behaviours.
Perhaps you should consider a section that allows you to "disable" strictAny for a block code?
The language "rust" has this nicely solved.
https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html
Just wanted to comment from the Google side. We both (1) eagerly turn on all the strictness flags you provide (we are glad to trade off development time for safety) and (2) use a linter to globally warn about using any
, but finally (3) rely heavily on using any
as a 'shut up type checker' sort of thing.
Whenever we turn on a strictness flag, we turn it on globally in our codebase, then sprinkle 'any' in all the currently-failing locations to make it easy for teams to individually fix their type errors. So I think this proposed strictAny would not be useful for us either, unless we had some other sort of escape hatch type. Preserving any
while bringing in other mechanisms to move code away from any
, like unknown
, feels like the right approach to me too.
PS: we love reading these rationales, thanks for writing it!
@DanielRosenwasser this seems like a sensible conclusion given the design constraints for TypeScript. I'll share my perspective on a couple points that led us to a different choice in Hack.
Users need to know never
TypeScript uses any
to shut up the type checker. For Hack we utilize error suppression comments instead. In fact there is no explicit way to write the any type within Hack, instead it is the type we use whenever type information is missing. So introducing a new type like dynamic
is less costly since we don't have an explicit any
type as well to support.
Didn't prove its worth for the cost
It is interesting your measure for if it was worth the cost is seeing if there are errors in existing code. The main motivation for us is to prevent future bugs from being introduced. In particular in a strict file for Hack, we've found developers have the expectation that the type checker will warn them when they are making mistakes. Next thing you know, they call a function that returns an any
type (missing a return type) and they are unaware they no longer have protection from the type checker. When this occurs the team is asked the same question, "Why did Hack not warn me I was working with an any
type?"
The dynamic
feature was a response to this. Another approach we could've done is use the "strict" any
in strict mode files. It is something we discussed (and may still do), but ultimately we saw enough value to make it it's own type in our type system.
Hey @dlreeves! From what you just posted, it definitely sounds like Hack had a good opportunity to introduce dynamic
. Like you might've seen from our enthusiasm in person, we do think the feature is useful, especially for new code. But TypeScript has formed a community with direction and momentum for these scenarios. Convincing people to switch from any
to dynamic
might be a long-running challenge, and disrupting people's working code with --strict
is what I alluded to by not paying off the cost for the approach we took. dynamic
isn't off the table, but it's not something we're actively pursuing.
TypeScript uses
any
to shut up the type checker. For Hack we utilize error suppression comments instead. In fact there is no explicit way to write the any type within Hack, instead it is the type we use whenever type information is missing.
That's actually really interesting. So my understanding is that in Hack, any
is in the type system, and has relatively similar semantics, but only arises implicitly (i.e. users can't use it explicitly), and implicit any
s are disallowed in Hack's strict mode (which is similar to our noImplicitAny
).
Funny enough, TypeScript does have a suppression comment: // @ts-ignore
. It was partly motivated by some of the use-cases Google had (which @evmar alluded to above). If TypeScript had been designed at Google, maybe we'd have been using // @ts-ignore
instead of any
and have a similar story. 馃槃
For the record, we really enjoyed meeting with you and your team and hearing about the problems you were working on. The fact that some of the original work was inspired by Scala.js, and that we were inspired by your work, makes me happy that we can learn and collaborate.
Most helpful comment
I would add that, today, expressions of type
never
really only occur in places where the type system has determined that the value truly cannot be observed unless some constraint of the type system was violated. The upshot of that is that we have never had to really wrestle with what it means to havenever
appear in a property position... but if asserting tonever
becomes the commonplace way to get out of a type error, then it's going to be in input positions, and if it's in input positions then it'll eventually be in output positions, and once you have anever
in an output position you have to be real careful -foo<T>(x: T): T | string
producesany
if invokedfoo(null as any)
but producesstring
if invokedfoo(null as never)
, and the latter is even more unsound than theany
in the first place because it appears to provide a constraint that is definitely not guaranteed to be true.Throwing ourselves into a world where
never
is the default cast is a) confusing as hell and b) just adds epicycles like "Add a--noImplicitNever
switch to warn me whennever
values creep in to input positions unexpectedly".