Skip to content

What's happening with strictAny? #24737

Closed
@DanielRosenwasser

Description

@DanielRosenwasser

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

  1. the functionality of dynamic (i.e. a scoped version of any)
  2. without introducing another concept (i.e. a new dynamic type)
  3. 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 if any is assignable to number, 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 example

declare 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, since any is no longer assignable to Some & Very & Long<Type, Annotation | Have<Fun, Writing, This>>.

The workaround is actually very simple - use never instead of any:

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 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.

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 as any 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DiscussionIssues which may not have code impact

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions