Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New --strictAny mode #24423

Closed
wants to merge 8 commits into from
Closed

New --strictAny mode #24423

wants to merge 8 commits into from

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented May 25, 2018

This PR implements a new --strictAny compiler option in the --strict family of options. This mode forbids unsafe conversions from any to other types. Specifically, in --strictAny mode, any is assignable only to any and an empty object type ({}). The --strictAny option doesn't otherwise alter the special behaviors of any, i.e. unrestricted property access and function calls through objects typed as any continue to be permitted.

The --strictAny option effectively identifies transitions from the loosly typed world of any to the strongly typed world of other types and requires such transitions to be accompanied by proof (through control flow based type narrowing) or assumption of responsibility (through explicit type assertions).

An example:

declare function getValue(propName: string): any;
declare function setLimit(x: number): void;

let counter = getValue("counter");  // Inferred type is any
setLimit(counter);  // Currently permitted but error in --strictAny mode

The --strictAny mode requires such unsafe conversions to be accompanied by dynamic checks or explicit type assertions:

// Proof through control flow narrowing
let counter = getValue("counter");
setLimit(typeof counter === "number" ? counter : 0);  // Ok

// Assumption of resposibility
let counter = getValue("counter") as number;  // Trust me, it's a number
setLimit(counter);

The --strictAny option includes an exception for assignability of function types, permitting strongly typed parameters in the source to be matched by a rest parameter of type any[] in the target even though that is technically not safe. For example:

type AnyFunction = (...args: any[]) => any;
let f: AnyFunction = (x: string) => s.toLowerCase();
f(42);  // Ouch!

We could potentially remove this exception and instead require such general function types to use type never, which would ensure that it isn't possible to call the function without first explicitly asserting a more specific type:

type AnyFunction = (...args: never[]) => any;
let f: AnyFunction = (x: string) => s.toLowerCase();
f(42);  // Error, number not assignable to never

Note that --strictAny is potentially quite disruptive to code bases that use any liberally. However, in code bases that are intended to be strongly typed throughout it definitely can be both revealing and informative. For example, --strictAny identifies 100+ locations in the TypeScript compiler where we have unsafe conversions from any, many of which are unintended.

@yortus
Copy link
Contributor

yortus commented May 26, 2018

@ahejlsberg if I understand correctly, with --strictAny enabled, we get part way to having the proposed unknown top type in #10715, since any is no longer assignable to other types (but all other types are still assignable to any).

If property acceses and function calls to any values were also subject to proof or assertion, we'd be able to build projects where any would behave as a properly strict top type. We'd effectively have unknown through a compiler flag, without needing a new type.

Can you comment on why --strictAny only requires proof for assignments, but not property access and function calls? Is this a consideration for future strictness checks?

@ahejlsberg
Copy link
Member Author

If property acceses and function calls to any values were also subject to proof or assertion, we'd be able to build projects where any would behave as a properly strict top type. We'd effectively have unknown through a compiler flag, without needing a new type.

That is technically true, but I'm not sure it would ever be practical. I think the issue of introducing an unknown safe top type is largely orthogonal since there are definitely scenarios where you want to use both in the same code base. (And we are indeed considering introducing such an unknown type.)

Can you comment on why --strictAny only requires proof for assignments, but not property access and function calls? Is this a consideration for future strictness checks?

I think the core intuition people have with the any type is that it allows you to treat a particular object as being dynamically typed ("JavaScripty"), i.e. not subject to static checks when you perform operations on it. However, it seems far less intuitive that such objects implicitly convert to strongly typed values, and I believe this is where the majority of any-related issues arise.

@yortus
Copy link
Contributor

yortus commented May 27, 2018

I see your point that ideally we'd want both unknown and safe any in the same codebase.

I think the core intuition people have with the any type is that it allows you to treat a particular object as being dynamically typed ("JavaScripty"), i.e. not subject to static checks when you perform operations on it. However, it seems far less intuitive that such objects implicitly convert to strongly typed values, and I believe this is where the majority of any-related issues arise.

Yes I suppose that's true usually. One case that's still a problem is when the anys come from imported libraries that use them as eg return types, when they should be using {} | null | undefined (or unknown if it existed). Eg JSON.parse and literally thousands more on DefinitelyTyped. So the library effectively turns off type checking in parts of my calling code.

--strictAny will help with this case too, but in these cases I'd still like my project, not the library, to decide whether static checks should be disabled, including property access and calls. I guess that's a case for unknown (and lots of DT PRs) to solve.

EDIT: PR for unknown type is up now, so all my concerns are covered brilliantly. Looking forward to using both unknown and safe any.

@NN---
Copy link

NN--- commented May 27, 2018

Is it similar to TSLint rule?
https://palantir.github.io//tslint/rules/no-unsafe-any/

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jun 1, 2018

I've got to admit that after thinking about this PR, I do have some reservations. 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. Maybe that's just the consequence of turning --strict on though: you have to think more about what you're doing!

As a side note, I think it's kind of cute that never in a sense is the dual of any in this new mode. You can assign it to anything, but for the most part you can't do very much with it.

@Yogu
Copy link
Contributor

Yogu commented Jun 3, 2018

any is assignable only to any and an empty object type ({}).

Shouldn't this be {}|null|undefined (and/or unkown) with --strictNullChecks enabled?

@DanielRosenwasser
Copy link
Member

After a lot of discussion on the current implementation, we've decided that --strictAny isn't something we're actively going to be pursuing in the near future. This was due to a mix of complexity and ease-of-use concerns, and whether the merits outweighed them. You can read more on why at #24737.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants