Typescript: allow narrowing from any

Created on 28 Jul 2016  Â·  120Comments  Â·  Source: microsoft/TypeScript

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 https://github.com/Microsoft/TypeScript/issues/9999#issuecomment-238070961 where narrowing from any works!

try it: npm install typescript-narrowany

Breaking Change Committed Suggestion help wanted

Most helpful comment

"Just add a flag" eventually :arrow_right:
image

All 120 comments

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) {}

@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 https://github.com/Microsoft/TypeScript/issues/9999#issuecomment-237775880:

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.

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.

:+1: for a flag to cater to _both_ sets of users, since both groups have valid needs.

Serious question @aleksey-bykov -- how are there even any anys left in your codebase?

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

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

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?

You can do

type unknown = {}

right now if you want.

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.

@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

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

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

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

I would just like to repeat what I said in #8677, which is that you need to do something to make try-catching Error subclasses manageable while receiving the benefits of typechecking. That could be allowing instanceof to narrow any, it could be a new instanceof-like construct which narrows (maybe extends), it could be some kind of special construct for catches like the one @bali182 suggests in #8677. However right now the combination of decisions you have made has put users of custom exception classes in a terrible box.

I am writing an TypeScript program that uses exceptions, and right now I have had to multiple times include the following eight(!) line idiomatic construct just to catch an exception:

    } catch (_e) {
        if (_e instanceof SomeCustomError) {
            let e:SomeCustomError = _e
            console.log(e.someField) // Or whatever
        } else {
            throw _e
        }
    }

That's very awkward and contains a lot of repetition (and therefore potential for typos). Compare with the clear, compact analogous code in C#:

    } catch (SomeCustomError e) {
        log(e.someField) // Or whatever
    }

You should do _something_ to make this less awkward in TypeScript; allowing instanceof to narrow would help a lot while still resulting in code that looks like standard ES6.

This said, two things:

  • I think a compiler flag is not a good solution. I should be able to just write code, paste it into stackoverflow etc and have it be portable, a compiler flag means that some of my typing goes away unless I distribute with build metadata always. If this were some obscure edge case it would be one thing, but the case I'm working with is "catching an exception", which is a major feature.
  • A suggestion is made in this thread that one solution would be special-case allowing the exception in a catch clause to be typed as : {} because technically this opts back in to narrowing-instanceof. I think this is a really bad solution, this is extremely non-discoverable and it would be _very_ nonobvious to a layperson coming across TypeScript code what the : {} tag does. I had to read this thread a couple of times before I understood which of : {} or : any it is that would activate narrowing and why, and I've been writing Typescript for a number of months now. To a new user it would be even more baffling, the :{} would just look like ASCII gibberish.

Overall, you have an education problem right now around the surprising behavior of type narrowing when any is involved (ie instanceof works sometimes, then in the presence of an any silently loses some of its powers). As I note in a bug on the TypeScript handbook I filed last night https://github.com/Microsoft/TypeScript-Handbook/issues/353, the handbook is actually currently incorrect in how it describes the current behavior here! Ideally the solution to what to do with instanceof would make the education problem easier rather than worse.

@mcclure If it's any help you can reduce some of that boilerplate with something like this:


function narrowError<T>(e:{}, c:new(...x:any[]) => T):T {
    if (e instanceof c) {
        return e;
    } else {
        throw e;
    }
}

class SomeCustomError {
    constructor(public someField:string) {}
}

try {
    // ...
} catch (e_) {
    let e = narrowError(e_, SomeCustomError);
    console.log(e.someField);
}

@RyanCavanaugh but then how come this works?

function isArray(value: any): value is any[] {
     return Object.prototype.toString.call(value) === '[object Array]';
}
const something = Math.random () > 0.5 ? [] : 1;
if (isArray(something)) {
    console.log(something.length);
}

@aleksey-bykov because something is any[] | number ?

but it doesn't matter since isArray(value: any) declared with any, does it?

The parameter type of a type guard isn't used in the narrowing logic

:open_mouth: :gun:

it just blows out the rest of my mind, why the type of the parameter which is being narrowed isn't used? from what do we narrow then? why would we care to specify it at all?

It's used for the function body. I use it for generics Action<any> => Action<Payload>

I don't understand what the alternative would be. Many type guards can effectively determine any input value, and need to do some weird stuff in their implementation, so any as a parameter type makes a lot of sense. What else would you write?

Honestly, I treat any like I treat dynamic from C#. any means opt out of type-system. Not "this can be anything" nor opt-out a little bit. I think instead of changing the behavior of any ( which does what it should: out-out! period ) it would be better to introduce a new primitive like never something like dynamic or mixed that can be anything without opting out of type-system. just a thought though. The current behavior of any is very useful for rapid development. I use it at first then when the API is stable I remove all occurrences replacing them with solid types.

Besides, other problems the any type causes should be resolved on their own. we shouldn't change the behavior of any.

P.S. @aleksey-bykov an Array with extra params checked against Array.isArray will be run-time valid but compile-time invalid. it will break a lot of code to change this behavior.

well... i thought that there is no alternative at all to begin with, hence this topic - allow narrowing from any, which makes me feel silly because narrowing from any does work already, just probably not _to_ anything and not _from_ anything, and what those from and to are is a question yet to be explored

turns out i don't know what type guards really are, i thought that typguards are predicates that operate on a value of a type they are declared for

still trying to wrap my head around the idea that there are types (only one type?) that get ignored when used for parameters of type guards at least (anything else?)

and of course this one doesn't work:

declare const something : any;
function isArray(value: any): value is any[] {
     return Object.prototype.toString.call(value) === '[object Array]';
}
if (isArray(something)) {
    console.log(something.length);
}

how can i trust what i see after i've seen that:

function isArray(value: any): value is any[] {
     return Object.prototype.toString.call(value) === '[object Array]';
}
if (isArray(something)) {
    console.log(something.length);
}

which might or might not work depending what any is.... .... .... ... ..

@aleksey-bykov anything inferred/declared as any cannot change it's type to anything rather than any no matter what. type guards don't change the type of their parameter. they change the type of argument passed at call site. if that argument is any then it cannot be changed.

ok, i think i got it, it's just trickier than i though it was:

  • typeguards operate on a type of the argument not on the type of their parameter, the type of the parameters just sets the boundaries to the type of an acceptable argument

uff da, that's been a ride

wiki needs a topic on this, thanks

@aleksey-bykov do these explanations seem hand-wavy to you, as they do to me? As you said at the top of this issue, narrowing from any is natural and intuitive, but when it landed it broke lots of bad code.

I think the much simpler explanantion is that type guards _were_ intended to work with all types (since the team presumably values orthogonality in language features), but then came Dog|House Inc, then a pragmatic backing out, and then later a sort of _ex post facto_ justification of why this was the logical behaviour all along.

Nothing has changed. The request still stands. Narrowing from any must be fixed. The only thing that changed is my understanding what a type of a parameter in a type guard is. when you see isSomething (value: any): value is Something it doesn't mean shit. It is as good as isSomething (value: *): value is Something where * is not even a type but _a placeholder_ that matches an argument of _any type out there_, no pun intended.

More to that the syntax of functions and type guards should have been made different, for what goes as a type of a parameter means different things, and that confused the hell out of at least one person i know by looking 100% identical.

I propose that type guards narrow from all types except Dog|House, then everyone wins.

@aleksey-bykov for (value: any): value is Something the parameter type still has a purpose. The function body needs to be type-checked too. so:

// if obj is declared to be any then the function body won't check usage.
function isDog(obj: Animal): obj is Dog {
  // obj.says will be checked for spelling mistakes and stuff.
  return obj && obj.says === 'woof';
}

In my opinion any can't change it's behavior. But the feature request is very useful. There are many places where we can't use anything but any :sweat_smile: like: Action<any> or catch() etc. But introducing another primitive seems like a better way. like Action<mixed> and mixed is: number | string | boolean | symbol | Function | Object | mixed[] but not null | undefined this way we have a type we can narrow easily without breaking anything and have it check for existence :grin:

Introducing a flag to allow us to narrow from any wouldn't break anything.

@Seikho True, but that would mean "all or nothing" There are still thousands of type definitions in @types and DefinitelyTyped that need to be fixed. so if you set the flag you'll get a LOT of errors you can't fix since they are external typings. But with another primitive it's opt-in/opt-out instead of all/nothing.

Type guards are functions. Type definitions don't have function bodies. Ergo type definitions won't be broken by allowing narrowing from any.
Edit: To clarify, you can declare a type guard function, it can't be consumed inside type definitions.

Right right. even though narrowing doesn't only happen with type guards yet all type narrowings are done in function bodies. You are right, a flag could be useful, but I would still prefer opt-in out-out instead of all or nothing.

However those methods of narrowing can't be done inside type definitions.

Another way to look at this is that there are two TypeScript 'rules' that cannot possibly both apply to the same code at the same time:

  1. Anything declared as any will not be type checked _anywhere_ it appears in the code
  2. Type guards create a code region where the type of the guarded variable is _known_ to be of a narrower type

These two rules obviously clash when they both apply to the same piece of code, i.e. when the guarded variable is declared as any. Which rule should take precedence?

Clearly from the comments here and in related issues, there are pros and cons either way. Narrowing from any would enabled stricter type checking and better refactoring support. On the other hand there is existing code that is written on the assumption that rule 1 wins and would not compile otherwise.

Having a compiler flag would allow the individual _project_ to effectively specify which of the two rules above takes precedence according to the needs and design guidelines of that project. This is much like being able to opt in or out of strict null checks, or implicit any checks, on a per-project basis.

Importantly, setting or clearing such a flag _would not_ affect the meaning or the runtime behaviour of the code in any way, but just how strict the compile-time type checking is. Again that's a lot like the strict null/implicit any compiler flags.

@RyanCavanaugh

how are there even any anys left in your codebase?

Another case is when using third-party libraries. The .d.ts files on DefinitelyTyped must contain 1000s of any return types and any property types where nothing more specific could be written. As a random example from knockout.d.ts:

interface KnockoutBindingContext {
    $parent: any;
    $parents: any[];
    $root: any;
    $data: any;
    $rawData: any | KnockoutObservable<any>;
    $index?: KnockoutObservable<number>;
    $parentContext?: KnockoutBindingContext;
    $component: any;
    $componentTemplateNodes: Node[];

    extend(properties: any): any;
    createChildContext(dataItemOrAccessor: any, dataItemAlias?: any, extendCallback?: Function): any;
}

The intended meaning in most of these cases is _"it could be anything"_, not _"turn off type checking"_.

However if you are working with these properties and methods, you either (a) can't narrow, or (b) have to first cast to {} and then narrow.

I suppose you could say that all these 1000s of typings should be changed to {} to support narrowing. But I guess it demonstrates that the idea of using {} for _"it could be anything"_ types is currently not widely adopted.

it won't be ever adopted, narrowing from {} is so counter-intuitive, to the point where it hurts physically

@RyanCavanaugh, how can you _narrow_ from um... nothing at all?

@aleksey-bykov It's only counter-intuitive if you mix up the size of the set of values matched by a type, and the number of rules/knowledge written in that type. The two are inversely proportional.

The syntax {} should be read as "zero knowledge", i.e. "I know nothing about this value.", which is equivalent to "all values". The phrase "narrowing" doesn't refer to a narrowing of knowledge (which would be impossible on {} because it is already zero knowledge), but of the set of matched values, which is a _widening_ of knowledge.

It can be too easy to read {} as a bottom/unit type rather than a top type, i.e. "this object is empty" rather than "you don't know anything about this object". You may prefer to write Object instead of {} in that case.

@jesseschalken all true about {}, but it still misses the point that when it comes to using third party typings, we have to deal with the fact that it's most common to find any used to mean _zero knowledge_, without the library/typings author having any intention of disabling type-checking in their consumers' code.

@jesseschalken i understand the reasoning behind it, i am just emphasizing the fact that due to having to explain things like:

The syntax {} should be read as "zero knowledge", i.e. "I know nothing about this value."

the choice of {} syntax is rather poor for a type that means "zero knowledg yet to be discovered" rather than "empty type" (which is what it is called in TS internally) meaning "go away, there is nothing inside me" according to the traditional meaning of {} object literal, hence my remark

on a constructive vibe, it might not be too late to add a standard meaningful alias to it, say, undiscovered ( @RyanCavanaugh ) to avoid further massive confusion among developers:

// lib.d.ts
type undiscovered = {};

it's a sad thing that in search for a balance between keeping the syntax to minimum to avoid conflicts with upcoming JS features and clarity the design team would rather sacrifice the clarity by pumping multiple meanings into the existing syntax to the point where it stops making sense

@yortus I certainly agree the common use of any when {} was intended is unfortunate.

@aleksey-bykov I'd say the team has never wanted there to be confusion with what the types mean. Rather, practices have evolved organically as the type-checking capabilities of the compiler have got better and better. And we have now reached the point where the past and ongoing confusion about what any and {} mean is becoming increasingly apparent as people want to exploit the type system to write ever-stricter and more readable code.

Perhaps the only 'mistake' has been that, AFAIK, there has never been any clear guidance directing people to use {} rather than any when they mean _"this could be anything"_, since without guidance any seems the far more obvious annotation to use, and {} does not intuitively look right at all (it _looks_ like you are saying _"this thing is a plain object"_).

Indeed, we can see on DefinitelyTyped that's exactly what just about everyone (including the TypeScript team - I'm looking at you lib.d.ts) does. And so now here we are...

honestly i would NOT have started this topic if i knew that {} can do what i expected from any, namely being a yet-to-be-narrowed type

here comes a question though:

  • would it be a breaking change to replace all any in lib.d.ts to {}?

which is rather a rhetorical question, but this is what i will be doing now in my local copy of lib.d.ts anyway to take full advantage of being able to narrow

ugh... what a mess!

would it be a breaking change to replace all any in lib.d.ts to {}?

I imagine it would break all sorts of existing code that has taken advantage of the _non-type-checked_ nature of any types even though the library/typings author only intended to mean _"this could be anything"_.

It would also be ironic to propose a big breaking change to make that switch, given that the rationale for disabling narrowing on any in the first place was to avoid a breaking change.

(updated the original request with the latest findings)

Looks like the TypeScript team also uses any rather than {} to mean _"this could be anything"_. I did a search in lib.d.ts for properties and function return types annotated with either any or {}, since these are the likely 'output' values from the platform that consuming code might like to work with and narrow from.

To find these instances I searched lib.d.ts for the exact strings : any; and : {}; respectively (note the ; to avoid picking up parameter annotations that usually represent values passed _into_ the platform).

In lib.d.ts I found:

  • 91 instances of any used as a property type or function return type
  • 0 instances of {} used as a property type or function return type

If I just search lib.d.ts for _all_ occurrences of : any and : {}, I find 390 of any, and 1 of {}.

EDIT: There are also 1930 instances of => any. But these are almost all event handler signatures, where the return value is passed from consumer code _to_ the platform, so not things that would be narrowed in consumer code. Still, they should all be => {} if that was supposed to be the proper way to say _"this callback can return anything"_.

As things currently stand, since lib.d.ts is part of TypeScript, and {} means _"it could be anything"_ and any means _"disable type-checking on this value"_, then it should be considered a bug that any has been used throughout lib.d.ts to mean _"it could be anything"_, since it is not intended (or correct) to disable type checking in consumer code.

And fixing that bug would be a breaking change.

How about we compare the actual meaning of any and {} with the commonly understood meaning by looking at some popular declaration files from DefinitelyTyped.

Summary (TL;DR)

Actual meanings (as noted already in this issue and elsewhere):

  • any means _"opt out of type-checking for this value; don't even allow narrowing"_
  • {} means _"this value could be anything; it must be narrowed before anything useful can be done with it"_

Commonly understood meanings (going by usage trends on DefinitelyTyped including official typings like lib.d.ts):

  • any means _"this value could be anything"_
  • {} means _"this value is a plain object or property bag"_

Details

Here's what I found for some popular libraries on DefinitelyTyped (with simple search for : any and : {}):

| library | uses of any | uses of {} |
| --- | --: | --: |
| angular.d.ts | 216 | 1 |
| async.d.ts | 67 | 0 |
| backbone.d.ts | 142 | 2 |
| bluebird.d.ts | 74 | 0 |
| chai.d.ts | 85 | 8 |
| d3.d.ts | 96 | 2 |
| ExtJS.d.ts | 2082 | 0 |
| jquery.d.ts | 179 | 1 |
| knockout.d.ts | 173 | 3 |
| lib.es6.d.ts | 467 | 1 |
| lodash.d.ts | 331 | 7 |
| node.d.ts | 254 | 0 |
| passport.d.ts | 28 | 0 |
| Q.d.ts | 65 | 0 |
| react.d.ts | 273 | 4 |
| restify.d.ts | 72 | 0 |
| rx-lite.d.ts | 81 | 0 |
| sinon.d.ts | 121 | 0 |
| sqlite3.d.ts | 31 | 0 |
| three.d.ts | 453 | 28 |
| typescript.d.ts | 28 | 0 |
| underscore.d.ts | 302 | 0 |
| webpack.d.ts | 29 | 0 |
| winrt.d.ts | 1121 | 0 |

The use of any in type declaration files

Many (all?) of those 1000s of any annotations are bugs according to the 'proper' meaning of any. Library declarations _do_ have values that _"might be anything"_, but they _do not_ have the intention (or right) to cause consuming code to be _"opted out of type-checking"_. So in theory typings files should always use {} and never use any for _"could be anything"_ values.

The use of {} in type declaration files

In the cases where I could understand the API (e.g. by finding the relevant docs), {} has been used to denate a "property bag", i.e. a plain object whose key/value pairs are known only at runtime. This is in contrast to the true meaning of {} which is _"zero knowledge"_. These actual usages are intented to imply specific knowledge, e.g. _"this is a plain object"_ and _"this is not a primitive"_.

For example:

  • lib.es6.d.ts uses {} for a contextAttributes parameter that is expected to be a property bag.
  • knockout.d.ts uses {} for "bindings" objects, which are plain objects used for their name/value pairs
  • jquery's famous $(x) notation uses object: {} for the overload that expects object to be a plain object
  • chai.d.ts uses both {} and Object to annotate the obj parameter in a bunch of functions where obj is expected to be an object instance (i.e. not a primitive)
  • backbone.d.ts uses {} to denote a plain object used as a model, as in add(model: {}|TModel, options?: AddOptions): TModel

Conclusion

From all this I think its safe to conclude that _in common understanding_:

  • {} means _"this is a plain object or property bag"_
  • any means _"this could be anything"_, and libraries using any do not imply that such values:

    • can be used any way consumer code thinks is valid without first narrowing them

    • should silently fail to be narrowed even when consumer code does try to narrow them before use

Out of curiousity, is the Dog|House case the only blocker against narrowing any?

Since we're discussing usage of any vs. {}, I figured I'd throw in my thoughts on that.

Here's the thing: in a parameter position, it really doesn't matter whether you use {} or any because they're both effectively the "top" types. So given any vs {} when declaring a parameter, using any is significantly clearer for those who don't know what {} means. They both say "give me anything and I'll be happy".

When you're getting a value out of something, it's _incredibly frustrating_ to be unusable off the bat. This was especially true back before type predicates, and even truer before narrowing at all. That's typically why return values will be any instead of {}. The first thing you have to do with {} is narrow it down or use a type assertion, but that's just getting in your way.

in a parameter position, it really doesn't matter whether you use {} or any because they're both effectively the "top" types.

@DanielRosenwasser I'd add two qualifications:

  1. It depends whether you are calling the library, or the library is calling you. For example the declaration for JSON#parse is parse(text: string, reviver?: (key: any, value: any) => any): any;. If you call JSON.parse and provide a reviver callback, then you _receive_ the value parameter that's annotated with any. OTOH the => any return type annotation is harmless since that's something the library receives from you.
  2. When you say 'it really doesn't matter', I'd say that's technically true, but surely when you see it all over .d.ts files in 'harmless' parameter positions, doesn't it still add to the impression/confusion that any means _"this could be any type"_ rather than _"opt out of type checking"_?

so i gave {} a try in a real projects as a "narrowable-any" type and immediately found out that it is not assignable from:

  • void
  • null
  • undefined
var one : {} = null; // problem
var two: {} = undefined; // problem
var three : {} = <void> undefined;  // problem

so a real narrowable-any type would be: type unknown = {} | null | undefined | void

type unknown = null | undefined | void | {};
var one : unknown = null; // works
var two: unknown = undefined; // works
var three : unknown = <void> undefined; // works

When you're getting a value out of something, it's _incredibly frustrating_ to be unusable off the bat.

@DanielRosenwasser I agree with this. But its _also incredibly frustrating_ that you get no type-checking or narrowing on a value coming out of a library even when you make explicit effort to write defensive type-safe code, eg by narrowing before assuming properties exist. You are simply opted-out by the library's choice to use any, and there's no way to opt back in short of type casts or introducing dummy variables. At least being able to narrow on any, for those who value type-checking in all their code, would be a good compromise.

@DanielRosenwasser

it really doesn't matter whether you use {} or any because they're both effectively the "top" types

turned out that any is topper than {}

Can we just solve Dog|Animal case and remove blockers?

Modifying https://github.com/Microsoft/TypeScript/issues/5930#issuecomment-162135447 and using suggestion #9946:

function fn(x: any) {
  if (x instanceof Animal) {
    declare x: Dog; // User knows better than the type system, so let it go explicitly
    x.woof(); // Now allowed
  } else {
    declare x: House;
    // handle House case
  }
}

9946 will make narrowing any much less painful.

turned out that any is topper than {}

@aleksey-bykov unless you are assigning the other way, in which case any acts like a bottom type (since you can assign it to every other type), except that never is bottomer because you can't assign any to never but you can assign never to any.

@SaschaNaz I agree with the rationale behind #9946, however adding new syntax to do what the type guard is already doing seems harder and more verbose that just providing a flag to narrow on any.

There's no theoretical objection to narrowing on any, just existing code that would no longer compile. But a simple flag would allow that code to keep compiling, as well as fixing the problem #9946 aims to fix but without adding any new syntax.

Ranty McRantFace

I sort of wish we'd stop refering to any as a type, since any is not a type at all in a self-consistent type system (see below).

any is probably better described as a keyword that may appear in a type position to indicate that a value should not be type-checked. It is provided as a convenience to facilitate gradual typing, to indicate the parts of the program that we do not wish to be typed (yet).

Good behaviour: _facilitating_ but _not forcing_ type-safety

Having the any concept enables TypeScript to do a brilliant thing: to _facilitate_ writing statically type-safe code, _without forcing_ you to adopt static type safety everywhere.

Bad behaviour: _disabling_ type safely even when I want it

Now, I love that TypeScript _facilitates_ and _doesn't force_ type safety. But I don't like that it _disables_ type safety in parts of my code where I didn't opt out and would rather write type-safe code (e.g. narrowing the catch variable or the any values coming from third-party libraries, before assuming they have certain properties).

@DanielRosenwasser has pointed out that _"When you're getting a value out of something, it's incredibly frustrating to be unusable off the bat."_. Fine then, _allow_ people to use such a value in a non-type-safe manner, accessing properties without checking what it is first. It's just like JavaScript again! So convenient! No compile time errors, only runtime errors!

But please don't _disallow_ my type guards from working - I'm trying to write type-safe code! It's not my fault the library used any instead of {}. I _want_ to see a compile-time error when do something invalid to a variable that I've just narrowed. I _want_ refactoring to find the references to types that I've explicitly narrowed. That's why I'm using TypeScript and not JavaScript!

Aside: Is any a type? No, and here's proof:

_Proof by contradiction:_ Let's define a type-safe system as one where assigning from one type to another type is allowed _if and only if_ the types have a subtype/supertype (i.e. 'is-a') relationship, like this:

interface Animal {species}
interface Dog extends Animal {woof} // so, Dog is-a Animal
let animal: Animal;
let dog: Dog;

animal = dog; // OK: every Dog is an Animal
dog = animal; // ERROR: not every Animal is a Dog

Now let's assume that there exists a type T that is assignable both _to and from_ every other type in the type system (just like any). Such a type must permit the following code to pass type checking with no errors:

let mysteryThing: T; // T is the type that is assignable to/from all other types

mysteryThing = dog; // OK: every Dog is a T
dog = mysteryThing; // OK: every T is a Dog

mysteryThing = animal; // OK: every Animal is a T
animal = mysteryThing; // OK: every T is an Animal

Put those together, and we have:

  • every Animal is a T
  • every T is a Dog
  • therefore _every Animal is a Dog_

But we already know _not every Animal is a Dog_ (since dog = animal is an error). So we have a contradiction!

So, within our definition of a type-safe system, the assumption that T exists leads to a contradiction, so we conclude there is no such type T.

Finally, since in terms of behaviour T is just any by another name, and T can't be a type, then any can't be a type either. So it's something else - an annotation that means _"don't type check here"_.

@yortus I don't see why you needed to write a formal proof. Doesn't any break a type system _by definition_? Isn't that the whole point of it?

@jesseschalken that's why it's an aside below a rant. Really I think I was trying to work out the formal reasoning for myself, given the terms throwing around in this thread like 'top types' and 'bottom types'. EDIT: Even the spec calls it a type and says it's the supertype of all types (top type!)

Anyway yes, breaking the type system _is_ the point of any, but after looking through all those .d.ts file I realised most people think any is just a type that _"could be anything"_ (top type!), when in fact it's not a type at all but a request to the compiler to disable type-checking around a value, which is not something they really meant to do to their consumers' code.

If TypeScript just used a keyword like dynamic (like C#) or unsafe instead of any (which _sounds_ like a top type) it would have saved everyone a great deal of pain.

@jesseschalken I failed to convince TS team two years ago... http://typescript.codeplex.com/discussions/551687

@SaschaNaz that's a fascinating link. It contains pretty much all the observations, arguments and counter-arguments made in this issue, even anticipating many features that TypeScript did not have at the time, including unions, discriminated unions, type guards, and type aliases.

Still, I can understand resistance to the codeplex issue (massive breaking change) and to #9946 (requires adding new syntax).

However, _this_ suggestion facilitates improved type safety on an opt-in basis with no breaking changes and no new syntax, and all the machinery for it is _already_ in the compiler. People who want or need to maintain sloppily-typed code would be unaffected. I can hardly think of a more ideal match for TypeScript's goals.

@SaschaNaz also interesting the discussion on the codeplex issue between yourself and @danquirk where he points out that any is like a discriminated union type of all other types, and both he and you mention that in other languages you would need to do pattern matching before accessing members of such a type. He points out pattern matching an infinite number of types would be impossible, so TypeScript just allows you to access anything without matching the type first.

Well those objections to pattern matching no longer really exist since we have union types and type guards. Union types _do_ now require narrowing before member access, and type guards permit pattern matching on only the cases you are interested in, so the 'matching infinite cases' problem no longer exists.

So I think that argument simply no longer applies, leaving just the breaking change problem, which this issue addresses by making it an opt-in compiler option.

If anyone wants to play around with narrowing from any, I've implemented it in a branch (it's a very minor change):

  • try it out: npm install typescript-narrowany
  • see the diff: diff

It adds a boolean narrowFromAny compiler option which is false by default (so its a non-breaking change).

@aleksey-bykov would you consider putting a link to this in the issue description so people coming to this issue can try out an implementation of your suggestion on their code?


Here is an example of it in action:

// @narrowFromAny: true


class Animal {species}
class Dog extends Animal {woof}
class House {rooms}

// By contract, this function accepts only a Dog or a House
function sloppyDogHouse(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(); // ERROR: Property 'woof' does not exist on type 'Animal'
    x.speccies; // ERROR: Property 'speccies' does not exist on type 'Animal'
  } else {
    // handle House case
  }
}

function betterDogHouse(x: Dog|House) {
  // 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(); // OK
  } else {
    // handle House case
  }
}


// Narrowing the catch variable
function tryCatch() {
  try {
    // do stuff...
  }
  catch (err) {
    if (isFooError(err)) {
      err.dontPanic(); // OK
      err.doPanic(); // ERROR: Property 'doPanic' does not exist on type '{...}'
    }
    else if (err instanceof Error) {
      err.massage; // ERROR: Property 'massage' does not exist on type 'Error'
    }
    else
      throw err;
  }

  function isFooError(x: any): x is { type: 'foo'; dontPanic(); } {
    return x && x.type === 'foo';
  }
}


// Narrowing something returned from a library
function decode(code: string) {
  let val = JSON.parse(code);
  if (Array.isArray(val)) {
    val.foreach(el => {/***/}); // ERROR: Property 'foreach' does not exist on type 'any[]'
  }
  else {
    //...
  }
}

With --narrowFromAny, the compiler picks up five errors in the above code as shown in comments. With --narrowFromAny off or not specified, the compiler picks up no errors, not even the four typos.

I cases where something is considered a best practice, would it make sense for it to be opt-out instead of opt-in?

Opt-in in TS 2.1, and opt-out in TS 3.0 (BC break).

If the majority of users will or should be using something, it seems unfortunate to have to add yet another compiler option to our tsconfig files.

@glen-84 I guess it's the same thing with other 'best practice' flags like strictNullChecks. It's just to big a break to make it the default right away (even though getting the old behaviour is a only compiler option away). But as you say, perhaps it could be done after enough warning and adoption time.

Are we cool with the else branch of narrowing that frankly should be any - A rather than any again?

https://github.com/Microsoft/TypeScript/issues/4183

@aleksey-bykov Infinity - 3 is still Infinity, so I think it's ok for any - A to be any. #4183 would be even more accurate, although I don't think not having that should be a blocker for this.

any consists of finite number of types

@chicoxyzzy finite? So how many are there altogether?

@yortus obviously as much as TypeScript has, no?

or you count custom types too?

all types that have been already and yet to be written, so technically it's an unknown number at best

ok then. it's infinite
(dunno why I thought about basic types only ¯_(ツ)_/¯)

Even with basic types, there's a literal type for every possible string, and there are no defined limits on string length, so there's an infinite number just of string literal types, before we start even composing them into arbitrary interfaces, unions, intersections, etc. So yes, it's infinite (at least within the practical limits of storage space and processing speed).

@yortus isn't string type for every possible string? Are we talking about same _basic types_?
https://www.typescriptlang.org/docs/handbook/basic-types.html

@chicoxyzzy any includes _all_ types, not just basic types. It's getting off-topic now, but there are even an infinite number of basic types, since they include enums (infinite number of distinct types here), tuples (infinite number of these types too) and arrays (which are generic with an infinite number of element types).

Too long discussion for this simple topic, we should just merge narrow-from-any and close this.

we should just merge narrow-from-any and close this.

I don't think it's as simple as that. :smiley:

I don't think it's as simple as that.

So you at least don't like the flag idea, can you explain more? I thought the flag will satisfy us.

"Just add a flag" eventually :arrow_right:
image

I fully appreciate the reluctance to add flags. That should always be a last resort when things can't be solved any other way.

But I hope there is also a level of appreciation on the TypeScript team that the compiler is failing us in some basic ways here, and it feels like the problem is being ignored.

TypeScript's # 1 design goal is _"Statically identify constructs that are likely to be errors."_

Currently TypeScript does not catch the obvious errors below. It's low-hanging fruit implementation-wise. And since Dog|House must not be broken, that seems to leave either (a) a flag, (b) ignore the problem, or (c) ???

function decode(code: string) {
    try {
        let val = JSON.parse(code);
        if (Array.isArray(val)) {
            val.fourEach(el => {/***/}); // typo! Not caught by TSC
        }
        else {
            //...
        }
    }
    catch (err) {
        if (err instanceof Error) {
            console.log(err.messge); // typo! Not caught by TSC
        }
    }
}

I would propose that we narrow any if a) --noImplicitAny is on and b) the _declared_ type is not any (e.g. in that example, we would narrow, but not if it were let val: any = JSON...). Thoughts?

I'm not using --noImplicitAny (not yet at least), so it wouldn't be useful in my case.

Narrowing from any seems totally logical to me. Before some point the type was unknown or one of multiple, but now I wish to run code on condition that it matches a specific type (or sub-type).

I would propose that we narrow any if a) --noImplicitAny is on and b) the declared type is not any

@RyanCavanaugh wouldn't a) and b) clash with each other in some cases? For example if we add a replacer callback in the same JSON.parse example:

function decode(code: string) {
    let val = JSON.parse(code, (key: string, val) => { // ERROR: 'val' is implicitly 'any'
        if (Array.isArray(val)) {
            return {/***/};
        }
        return val;
    });
}

...that won't compile under --noImplicitAny, but if we change the val parameter to val: any so it compiles, then it won't narrow.

But you could just write val: {} if you wanted it to narrow to Array? Alternatively, someone could fix #4241, since that would cause val to get a contextual any type

But you could just write val: {}

You can already do that today. If people wanted to do that this issue wouldn't need to exist.

At some level, I just don't understand. If you don't want unsafe object access, don't use any -- it's the _entire point_ of the type. {} is a much better type if you're going to be guarding and doing things 'by the book". There are a few places where you can't help but have any and I'm sympathetic to those scenarios. But everywhere else, it's just a betrayal of our stated goal of the any type to have it suddenly start reporting errors on use.

I'm specficially thinking of code like this

let x: any = ...;
if (Array.isArray(x)) {
  x.randomProp = 10;
}

Let's say we narrowed x here, but you _did_ want to set randomProp. What now? Type assert x _back_ to any? What if you have a bunch of undeclared properties you want to set?

Array.isArray is not a type guard to begin with so the users won't feel a thing, but even if they do it's a good thing, let's break some sh*t code for a change, at the end of the day some one has to own it and be responsible for the quality of it, let's make it gradual let's add a flag

I see your point in the randomProp example. What I think is not getting much attention is this very similar situation:

let x = /*some expr returning an any*/;
if (Array.isArray(x)) {
  x.forEch(el => {/***/});
}

Same with your example, the compiler currently looks at that and says _"I guess you meant to access random property forEch" since it's an any type"_. But it's just a typo. And for me at least this is the far more common scenario. I woundn't even mind getting an error if I had something like the randomProp example, which I'd then rewrite to x['randomProp'] = 10; which more clearly shows the intention to use it like a property bag, rather than a property access typo.

Array.isArray is not a type guard to begin with...

actually it is, typed as isArray(arg: any): arg is Array<any>;

actually...

that (!) is an example of a change that breaks (how come? what about the users?)

my lib.d.ts snapshot at 1.6 doesn't have it

who ever needs a bag interface MyArraySwissKinfe<a> extends Array<a> { [key: string]: any; }

@RyanCavanaugh let's be intelligent we are trying to stick 4 pigeons into 3 cages, we need:

  • either more cages (types)
  • or less pigeons (flags)

Making Array#isArray a type guard _wasn't_ a breaking change because we don't narrow from any. Hopefully the relevance of that can be appreciated: it makes _fewer_ constructs an error rather than more.

@RyanCavanaugh's noImplicitAny suggestion + #4241 fix might be better than nothing. But it still seems to leave all the jumping-through-hoops to those whose just want type checking in well-structured code. For example these three would all do different things even through x is any in all three:

var x; /*now do stuff with x...*/       // ERROR: x is implicitly any
var x: any; /*now do stuff with x...*/  // OK but x cannot be narrowed
var x = funcReturnsAny();               // OK and x can be narrowed, even though it's still 'any'

I could live with that but it's subtle.

@yortus @RyanCavanaugh i seriously don't like the idea with implicit-non-narrowable and explicit-narrowable any, if i got it right (which i doubt because it's well tricky), how are you going to explain it to people? (i am first in line btw)

let x: any = ...;
if (Array.isArray(x)) {
  x.randomProp = 10;
}

... I still don't understand why you want x to be any inside a block that clearly limits the type. At runtime x would _have_ to be of type array, so why allow any non-array operations in that context?

Major versions exist for breaking changes, so I have to agree with @aleksey-bykov – let's break some sh*t code for a change. =)

making changes to the original design is a natural process, you can't be bound to the mistakes of youth till the end of your life, there is time to man up, face them and get them fixed

in terms of software development they call it refactoring, a non-popular but essential measure to keep your product afloat for upcoming challenges

here we are all looking at a problem and everyone agrees it needs a fix (show me a person who actively against fixing it, besides the design team)

there must be at least one serious breaking change per a major release, that's the toll of keeping things manageable and moving forward:

  • oh, you want async await? get you any fixed first!

I reran the fork against our RWC suite so I could show the actual examples of breaks we encountered.

From Angular

      hasShadowRoot(node): boolean { return node instanceof HTMLElement && isPresent(node.shadowRoot); }
                                                                                          ~~~~~~~~~~
!!! error TS2339: Property 'shadowRoot' does not exist on type 'HTMLElement'.

This doesn't seem like a helpful error (though it could have been, if shadowRoot were innerHtml or something else that was an actual "miss")

    function _normalizeBindings(bindings: Array<Type | Binding | any[]>,
                                res: Map<number, _NormalizedBinding | _NormalizedBinding[]>):
        Map<number, _NormalizedBinding | _NormalizedBinding[]> {
      ListWrapper.forEach(bindings, (b) => {
        if (b instanceof Type) {
          _normalizeBinding(bind(b).toClass(b), res);
                                            ~
!!! error TS2345: Argument of type 'Function' is not assignable to parameter of type 'Type'.
!!! error TS2345:   Type 'Function' provides no match for the signature 'new (...args: any[]): any'

Not helpful at all.

From other codebases

                    else if (value instanceof Date) {
                        return value.localeFormat(format || "G");
                                     ~~~~~~~~~~~~
!!! error TS2339: Property 'localeFormat' does not exist on type 'Date'.
                    }

Not really sure why they didn't declare localeFormat as a Date augmentation, but it clearly works for them in reality

            if (error instanceof Error) {
                if(error && error.number && (typeof error.number === "number")) {
                                  ~~~~~~
!!! error TS2339: Property 'number' does not exist on type 'Error'.
                                                          ~~~~~~
!!! error TS2339: Property 'number' does not exist on type 'Error'.

Not useful at all

linq.ts

            SequenceEqual(second: Enumerable<T>, compareSelector?: Transform<T, any>): boolean;
            SequenceEqual(second: T[], compareSelector?: Transform<T, any>): boolean;
            SequenceEqual(second: any, compareSelector?: Transform<T, any>): boolean
            {
                if ((second instanceof ArrayEnumerable || second instanceof Array)
                        && (!compareSelector) && FromArrayOrEnumerable(second).Count() !== this.Count()) {
                                                                       ~~~~~~
!!! error TS2345: Argument of type 'any[] | ArrayEnumerable<any>' is not assignable to parameter of type 'Enumerable<any>'.
!!! error TS2345:   Type 'any[]' is not assignable to type 'Enumerable<any>'.

Not useful

RxJS

          while (++index < len) {
            const sub = _subscriptions[index];
            if (isObject(sub)) {
              let trial = tryCatch(sub.unsubscribe).call(sub);
                                       ~~~~~~~~~~~
!!! error TS2339: Property 'unsubscribe' does not exist on type 'Object'.

Not useful, but could be

    export function subscribeToResult<T, R>(outerSubscriber: OuterSubscriber<T, R>,
                                            result: any,
                                            outerValue?: T,
                                            outerIndex?: number): Subscription {
      let destination: Subscriber<R> = new InnerSubscriber(outerSubscriber, outerValue, outerIndex);

      if (destination.isUnsubscribed) {
        return;
      }

      if (result instanceof Observable) {
        if (result._isScalar) {
          destination.next(result.value);
                                  ~~~~~
!!! error TS2339: Property 'value' does not exist on type 'Observable<any>'.

Not useful, but could be

      constructor(iterator: any,
                  project?: ((x?: any, i?: number) => T) | any,
                  thisArg?: any | Scheduler,
                  scheduler?: Scheduler) {
        super();

        if (isObject(project)) {
          this.thisArg = project;
          this.scheduler = thisArg;
        } else if (isFunction(project)) {
          this.project = project;
          ~~~~~~~~~~~~
!!! error TS2322: Type 'Function' is not assignable to type '(x?: any, i?: number) => T'.
!!! error TS2322:   Type 'Function' provides no match for the signature '(x?: any, i?: number): T'

Definitely not useful

VSCode

        private doElement(name: string, attributesOrFn?: any, fn?: (builder: Builder) => void): Builder {
                // Support second argument being function
            if (types.isFunction(attributesOrFn)) {
                fn = attributesOrFn;
                ~~
!!! error TS2322: Type 'Function' is not assignable to type '(builder: Builder) => void'.
!!! error TS2322:   Type 'Function' provides no match for the signature '(builder: Builder): void'

Not useful

    export function isNumber(obj: any): obj is number {
        if ((typeof (obj) === 'number' || obj instanceof Number) && !isNaN(obj)) {
                                                                           ~~~
!!! error TS2345: Argument of type 'number | Number' is not assignable to parameter of type 'number'.
!!! error TS2345:   Type 'Number' is not assignable to type 'number'.
            return true;
        }

Not useful

        public findMatches(searchString:string, rawSearchScope:any, isRegex:boolean, matchCase:boolean, wholeWord:boolean, limitResultCount:number = LIMIT_FIND_COUNT): editorCommon.IEditorRange[] {
            var searchRange:editorCommon.IEditorRange;
            if (Range.isIRange(rawSearchScope)) {
                searchRange = rawSearchScope;
                ~~~~~~~~~~~
!!! error TS2322: Type 'IRange' is not assignable to type 'IEditorRange'.
!!! error TS2322:   Property 'isEmpty' is missing in type 'IRange'.
            } else {

Not useful

        public hasActions(tree: tree.ITree, element: any): boolean {
            return element instanceof model.Expression && element.name;
                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type 'string' is not assignable to type 'boolean'.
        }

The only real _bug_ per se I would consider in this entire set.

@RyanCavanaugh could you share the total number of errors and the amount of code you tried it on?

fyi, 0 (zero) errors on our project:

C:\projects>node ./node_modules/typescript-narrowany/bin/tsc --project ./aps/master/src/Intelsat.APS.Web/scripts/src/tsconfig.json --diagnostics

Files:            1350
Lines:          236652
Nodes:         1130588
Identifiers:    422274
Symbols:        492611
Types:          163807
Memory used:   596481K
I/O read:        0.20s
I/O write:       5.70s
Parse time:      4.23s
Bind time:       1.27s
Check time:      9.02s
Emit time:       6.93s
Total time:     21.45s

@RyanCavanaugh thanks for taking the time to show the real world code affected by this, I think it 'completes the picture' of the pros/cons with this. The OP request was to _"Please consider putting it behind a flag provided the evident need for it."_ which seems the only way to get a win/win here. This issue was not (originally at least) aimed at breaking the existing code out there, but to address what I think is a legitimate desire for safer type checking, refactoring, etc.

So with the 'complete picture' in hand, could we settle on either:

a) add a new flag (the OP suggestion)

b) add a new meaning to an existing flag (@RyanCavanaugh suggested noImplicitAny)

c) nobody cares; wontfix; closed

d) taking every available complexity/configurability trade-off in favor of the latter eventually destroys software

Just to merge the long threads of this issue - this goes back to #1426.

The thing that has me unconvinced about a new flag (other than a desire to reduce n in the complexity term 2 ** n) is that it's not at all obvious why you would turn this one on, other than having figured out in the past that you wanted something to narrow and it didn't (which is necessarily subtle since it's not going to present as an error since it started as any). Obviously the 9 people in this thread would turn it on, but who else would? Would 95% of people who turned on noImplicitAny want it on? If so, let's merge the behavior and let people deal with a couple of breaks.

And of the breaks shown here, are there better fixes available that wouldn't require a flag? Maybe we shouldn't narrow any to Object, or maybe Function should be assignable to a type consisting only of call signatures? You can hack a lot of behavior with a flag or you can make correct fixes that help everyone.

e) brake it hard

i am good with {} | null | undefined | void for any since we own our *.d.ts, and it is a minor inconvenience to go over them one time with find/replace, although i wish we broke some code for the sake of consistency of upcoming features, but i am good, 8 people left

@RyanCavanaugh just to be clear, are you saying there is a possibility of addressing the above breaks you listed to see how they can be fixed, and making narrowing any on-by-default, with no flag?

I'd be thrilled if that was the case, but didn't think it was an option based on past comments. Sorry if I've misunderstood your remark.

We had an informal chat about this today and figured that narrowing any _except_ for typeof ... === 'object' and typeof ... === 'function' would probably be useful behavior. Those two narrowings don't produce a useful type (Object because it's uselessly close to {}, and Function because it's not properly assignable to things with call signatures yet still gets its untyped function call behavior which is the same you'd get from any) so we could leave them as any without much harm. The people doing x instanceof Animal and expecting to access Horse members are probably getting worse behavior than they'd like without realizing it and it would be a reasonable break.

few more cases:

the results of apply and call

const ouch = String.fromCharCode.apply(undefined, manyLittleThings);
const ugh = Array.prototype.slice.call(meIsArray);

Object because it's uselessly close to {}

I thought they were the same thing?

The distinction is super subtle but they are not identical. One observable difference is

let x: Object = { hasOwnProperty : 4 }; // error
let y: {} = { hasOwnProperty : 4 }; // ok
let y: {} = { hasOwnProperty : 4 }; // ok
y.hasOwnProperty('foo'); // also ok? huh?

Sorry for getting off topic, but shouldn't { hasOwnProperty : 4 } be an invalid object literal? It's just shorthand for let y = new Object(); y.hasOwnProperty = 4; and you can't assign a method to a number.

We can't have the notion of an invalid object literal. That's basically nonsense.

This all happens because of apparent members. These are properties that appear during property access and in the _source_ of an assignability check, but not in other places. They're there to mimic JS's prototype chain and boxing behavior. So when you write let x = { hasOwnProperty: 4}, that's fine, and we know that you can't call hasOwnProperty on x (because it shadows the copy given to it by Object#prototype). If you alias away a property (as shown in your example y) you can make a lot of bad things happen which is why we try to not make that happen in normal use.

Apparent members are also why you can invoke methods like toFixed or substr on primitives, even though primitives themselves have no members. The primitives gain the apparent members of their corresponding object types (strings get the apparent members of String, etc).

I see. So basically everything has the methods from Object, but compatibility with Object isn't actually enforced until something is assigned to Object directly. That's certainly not sound, but I assume it's just another compromise made to avoid scaring people with too much soundness, so meh.

:+1: for the behavior described at https://github.com/Microsoft/TypeScript/issues/9999#issuecomment-239058747

Summary:

  • Narrowing to primitives continues to work as it does today
  • Narrowing via instanceof or a user-defined type predicate will narrow _unless_ the narrowed-to type is exactly Function or exactly Object (in which case the expression remains of type any)
  • No commandline switch - this is the behavior in all cases

@yortus do you want to update your fork and own the PR for this? It should be a quick change compared to what you have -- just remove the switch logic and compare the target using === to globalFunctionType / globalObjectType

@RyanCavanaugh PR submitted: #10319 #10334

Was this page helpful?
0 / 5 - 0 ratings

Related issues

weswigham picture weswigham  Â·  3Comments

wmaurer picture wmaurer  Â·  3Comments

blendsdk picture blendsdk  Â·  3Comments

CyrusNajmabadi picture CyrusNajmabadi  Â·  3Comments

Antony-Jones picture Antony-Jones  Â·  3Comments