Skip to content

Union type is not narrowed by a Symbol property, even though it is correctly narrowed by a "regular" property #28701

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

Closed
solymosi opened this issue Nov 28, 2018 · 5 comments
Labels
Duplicate An existing issue was already created

Comments

@solymosi
Copy link

solymosi commented Nov 28, 2018

TypeScript Version: 3.3.0-dev.20181128

Search Terms: symbol, property, key, type inference, does not narrow, union type, different behavior of symbol and string keys

Code

const LOADED = Symbol();

class Entity {
  [LOADED]: true = true;
  doStuff() { }
}

class ReferenceToEntity {
  [LOADED]: false = false;
}
 
class SomeClass {
  entity?: Entity | ReferenceToEntity;

  foo() {
    this.entity && this.entity[LOADED] && this.entity.doStuff();
  }    //                                             ^^^^^^^ ERROR!
}

Expected behavior:
Since Entity[LOADED] is of type true and ReferenceToEntity[LOADED] is of type false, I would expect the type of this.entity to be narrowed to just Entity in foo() and the above example to compile, just like it does with string property keys – i.e. this behavior seems to be unique to Symbol properties.

Actual behavior:
Compilation error:

Property 'doStuff' does not exist on type 'Entity | ReferenceToEntity'.
  Property 'doStuff' does not exist on type 'ReferenceToEntity'.

Things that do not change the behavior:

  • marking [LOADED] as readonly
  • explicitly marking LOADED as unique symbol
  • turning [LOADED] into a getter, i.e.
    get [LOADED](): true { return true; }
    
  • disabling strict mode

Things that change the behavior:

  • using string property keys makes it work as intended, i.e. the compiler realizes that if this.entity.loaded is truthy then this.entity˙must be an Entity
  • assigning to entity in foo() just before the doStuff() call also makes it work:
    foo() {
      this.entity = new Entity();
      this.entity && this.entity[LOADED] && this.entity.doStuff();  // NO ERROR
    }
    

Playground Link:
http://www.typescriptlang.org/play/#src=const%20LOADED%20%3D%20Symbol()%3B%0D%0A%0D%0Aclass%20Entity%20%7B%0D%0A%20%20%20%20%5BLOADED%5D%3A%20true%20%3D%20true%3B%0D%0A%20%20%20%20doStuff()%20%7B%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20ReferenceToEntity%20%7B%0D%0A%20%20%20%20%5BLOADED%5D%3A%20false%20%3D%20false%3B%0D%0A%7D%0D%0A%20%0D%0Aclass%20SomeClass%20%7B%0D%0A%20%20%20%20entity%3F%3A%20Entity%20%7C%20ReferenceToEntity%3B%0D%0A%0D%0A%20%20%20%20foo()%20%7B%0D%0A%20%20%20%20%20%20%20%20this.entity%20%26%26%20this.entity%5BLOADED%5D%20%26%26%20this.entity.doStuff()%3B%0D%0A%20%20%20%20%7D%20%20%20%20%2F%2F%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5E%5E%5E%5E%5E%5E%5E%20ERROR!%0D%0A%7D

Related Issues:

@solymosi solymosi changed the title Property type not inferred properly when using Symbol key Union type is correctly narrowed by a "regular" property but not by a Symbol property Nov 28, 2018
@solymosi solymosi changed the title Union type is correctly narrowed by a "regular" property but not by a Symbol property Union type is not narrowed by a Symbol property, even though it is correctly narrowed by a "regular" property Nov 28, 2018
@ajafff
Copy link
Contributor

ajafff commented Nov 28, 2018

control flow only narrows element access when the property name is a string or number literal (for performance reasons), see #26424

@solymosi
Copy link
Author

@ajafff Oops, this is a duplicate then. Unfortunately this issue renders symbol properties unusable for our purposes without some not-so-clean workarounds, so it would be really nice to have #26424 extended with support for symbols.

@saitonakamura
Copy link
Contributor

@solymosi do you consider user defined type guards?

@solymosi
Copy link
Author

solymosi commented Nov 28, 2018

@saitonakamura They don't work either when implemented as methods on the classes:

const LOADED = Symbol();

class Entity {
    [LOADED](): this is Entity { return true; }
    doStuff() { }
}

class ReferenceToEntity {
    [LOADED](): this is Entity { return false; }
}
 
class SomeClass {
    entity?: Entity | ReferenceToEntity;

    foo() {
        this.entity && this.entity[LOADED] && this.entity.doStuff();
    }    //                                               ^^^^^^^ ERROR!
}

...unless the type guard is a separate function:

const isLoaded = (obj: Entity | ReferenceToEntity | undefined): obj is Entity => {
    return obj instanceof Entity;
}

class Entity {
    doStuff() { }
}

class ReferenceToEntity { }
 
class SomeClass {
    entity?: Entity | ReferenceToEntity;

    foo() {
        isLoaded(this.entity) && this.entity.doStuff();
    }    //                                  ^^^^^^^ IT WORKS!
}

The latter is actually the workaround I've implemented for now, but it does not feel that clean.

@weswigham weswigham added the Duplicate An existing issue was already created label Nov 28, 2018
@typescript-bot
Copy link
Collaborator

This issue has been marked as a duplicate and has seen no activity in the last day. It has been closed automatic house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants