Skip to content

allow narrowing from any #9999

Closed
Closed
@zpdDG4gta8XKpMCd

Description

@zpdDG4gta8XKpMCd

There are numerous questions as to why any cannot be narrowed from. Despite being natural and intuitive this feature was rejected due to being a serious breaking change to poorly written yet massive code bases.

Please consider putting it behind a flag provided the evident need for it.

TLDR

  • any is not what you think it is (poor naming at play)
  • for narrowing use type unknown = {} | undefined | null | void instead
  • yes, it's a hot unfortunate mess
  • no, there is no painless way to fix it
  • yes, you gonna need to fix your *.d.ts by hands
  • why? to make average JS developers happy with their less than perfect code
  • welcome to the club! get your badge at the reception

UPDATE
@yortus made a branch per #9999 (comment) where narrowing from any works!

try it: npm install typescript-narrowany

Activity

jesseschalken

jesseschalken commented on Jul 28, 2016

@jesseschalken
Contributor

Can you specify a use case? any is already compatible with everything and everything is compatible with it, so why do you need to narrow it?

The whole point of any is to opt out of static typing. If you don't want to opt out, but want a type that can be anything but which you have to narrow before assuming it is something, that's what {} is for.

This code passes:

function foo(x:any) {
    if (typeof x === 'string') {
        needs_string(x);
    }
}

function needs_string(x:string) {}

and so does this:

function foo(x:{}) {
    if (typeof x === 'string') {
        needs_string(x);
    }
}

function needs_string(x:string) {}
yortus

yortus commented on Jul 28, 2016

@yortus
Contributor

@jesseschalken here are some use cases:

A. Narrowing the error variable in a catch block, which is of type any and can't be annotated. The user did not 'opt-out' of type checking here, they just have no choice.

B. Retaining type safety when we explicitly use type guards on an any variable:

function validate(x: any) {
    x.whatever(); // no error, but that's OK since we've opted out of checking due to `any`

    if (Array.isArray(x)) {
        // we've EXPLICITLY opted-in to type checking now with a compile-time AND runtime array check

        x.Pop(); // oops typo, but no error!?
        x.whatever(); // no error!?
        x(); // no error!?
    }
}

C. Refactoring using tools like VSCode

Suppose in the following example we use the VSCode 'rename' tool to rename Dog#woof to Dog#bark. The rename will appear to work but will miss the guarded woof reference. The code will still compile without errors, but crash at runtime.

// An example taken directly from #5930 as a reason why any should NOT be narrowed
// By contract, this function accepts only a Dog or a House
function fn(x: any) {
  // For whatever reason, user is checking against a base class rather
  // than the most-specific class. Happens a lot with e.g. HTMLElement
  if (x instanceof Animal) {
    x.woof(); // Disallowed if x: Animal
  } else {
    // handle House case
  }
}

EDIT: Adding a fourth use case that came up later in #9999 (comment):

D. Consuming definitions from lib.d.ts and other third-party libraries. lib.d.ts and many (most?) other type declaration files on DefinitelyTyped use any to mean "this value could be anything". These values cannot be narrowed in consumer code, even though the consumer did not ask to opt-out of type checking.

yortus

yortus commented on Jul 28, 2016

@yortus
Contributor

I think @RyanCavanaugh summed up the issues around narrowing any:

On the one hand, listen to users, and on the other hand, listen to users...

However, TypeScript currently only caters to the one set of users.

👍 for a flag to cater to both sets of users, since both groups have valid needs.

RyanCavanaugh

RyanCavanaugh commented on Jul 28, 2016

@RyanCavanaugh
Member

Serious question @Aleksey-Bykov -- how are there even any anys left in your codebase?

jesseschalken

jesseschalken commented on Jul 28, 2016

@jesseschalken
Contributor

@yortus

A. Eeek. That's unfortunate. There should definitely be a compiler option to make caught errors {} instead of any.

B & C. I see. The problem then is that TypeScript enables you to mix dynamic and static typing, but you can't have the same variable be dynamically typed in one place and statically typed in another place. Dynamic vs static is a property of the variable itself and you can't transition in/out of dynamic mode without creating a new variable or using as with every reference, or redirecting references through a getter.

I don't care personally because I try to stay as far away from any as possible and consider the solution to any problems with any to be to simply remove or insulate myself from it. If the use case of mixed dynamic/static typing wrt the same variable is sufficiently common for mixed static/dynamic codebases I guess it's a worthy concern.

RyanCavanaugh

RyanCavanaugh commented on Jul 28, 2016

@RyanCavanaugh
Member

Unorganized thoughts, some contradictory

  • I find the "rename" scenario very compelling. Being able to safely refactor is a major thing for us and it feels like we're just screwing up here when seen through that lens
  • I don't think you get to have your strictness cake and eat it too. There are lots of good workarounds here -- declaring a parameter as {}, or as { [s: string]: any} and using string indexer notation to access unsafe properties, or writing const xx: {} = x; and narrowing on that once you're doing unsafe things with x. Seeing "real" code that can't reasonably accommodate those workarounds would be useful.
  • There have been a few suggestions for something like T extends any where you get strictness on the known properties of T but anys when accessing other properties. That seems like the both-sides-win behavior that avoids a new flag but accomplishes most of what's desired here
  • We should allow a type annotation of exactly : any or : { } on a catch variable. Why not.
  • Many problems described here would be well-mitigated by a type-based TSLint rule that disallowed dotted property access of expressions of type any. I'd certainly turn that rule on for all my projects
yortus

yortus commented on Jul 28, 2016

@yortus
Contributor

It's possibly just me, but I find {} to be a visually-confusing annotation that makes code intent less clear.

x: {} just looks at a glance like the code is saying 'x is an Object instance', when in reality it means 'x is absolutely anything but don't opt out of type checking'. So it might be an object, array, number, boolean, RegExp, string, symbol, etc.

Probably a stupid suggestion but is it worth considering adding a type keyword that's exactly like {} but more readable? Perhaps x: unknown or similar?

jesseschalken

jesseschalken commented on Jul 28, 2016

@jesseschalken
Contributor

You can do

type unknown = {}

right now if you want.

yortus

yortus commented on Jul 28, 2016

@yortus
Contributor

You can do type unknown = {} right now if you want.

True, as long as you don't mind adding boilerplate for than in every file you need it. Also type aliases lose their pretty name in compiler messages and intellisense.

zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Jul 28, 2016

@zpdDG4gta8XKpMCd
Author

@RyanCavanaugh

how are there even any anys left in your codebase?

  • JSON.parse - for a saved state which may vary
  • window.onMessage - for upcoming messages which may vary
  • narrowing exceptions (already mentioned)

but you are right, not that many left

jesseschalken

jesseschalken commented on Jul 28, 2016

@jesseschalken
Contributor

@yortus It depends how your project is set up, but if you define it at the top level, outside a module or namespace, it will be available across the entire project without needing to be imported, just like the things in lib.d.ts are. Fair point about compiler messages/intellisense though.

zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Jul 28, 2016

@zpdDG4gta8XKpMCd
Author

@RyanCavanaugh
one more case where we have any is for functions that deal with arbitrary input like deepCopy or areEqual

yortus

yortus commented on Jul 28, 2016

@yortus
Contributor

Dynamic vs static is a property of the variable itself and you can't transition in/out of dynamic mode without creating a new variable

@jesseschalken I assume you are pointing out this is how things currently are, not how they necessarily should be. From #5930 it's appears the team had no problem with the same variable transitioning from untyped to typed in the explicitly protected region of a type guard. After all, their first instinct was to implement narrowing from any.

It only got backed out when a subset of users, whose code is written according to the old proverb "just as all Animals are Dogs, so all non-Animals must be Houses", were offended by the compiler pointing out the flaw in their logic, and would rather silence the compiler than fix their code.

Having said that, their reasoning that "I used any to opt-out of all type checking" is also reasonable. However I agree with the team's apparent first instinct, that sure any does opt you out of type checking, but a type guard is a nice explicit way of saying "opt me back in".

117 remaining items

modified the milestones: TypeScript 2.0.1, TypeScript 2.1 on Aug 15, 2016
locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Breaking ChangeWould introduce errors in existing codeCommittedThe team has roadmapped this issueHelp WantedYou can do thisSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @glen-84@mcclure@zpdDG4gta8XKpMCd@DanielRosenwasser@yortus

        Issue actions

          allow narrowing from any · Issue #9999 · microsoft/TypeScript