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

Generic types not narrowed with instanceof guard #12085

Closed
crobi opened this issue Nov 7, 2016 · 7 comments
Closed

Generic types not narrowed with instanceof guard #12085

crobi opened this issue Nov 7, 2016 · 7 comments

Comments

@crobi
Copy link

crobi commented Nov 7, 2016

TypeScript Version: 2.0.3

Code

  class Left<T> {
    // private foo?: void;
    constructor(public readonly value: T) {};
  }

  class Right<T> {
    constructor(public readonly value: T) {};
  }

  function eitherNumberOrString(): Left<number> | Right<string> {
    return new Left(0);
  }

  // e is Left<number> | Right<string>
  const e = eitherNumberOrString();

  if (e instanceof Left) {
    // e is Left<number> | Right<string>, not narrowed to Left<number>
    // e.value is number | string, not narrowed to number
    e.value;
  }

Expected behavior:

Left<L> | Right<R> should be narrowed to Left<L> after using a instanceof Left type guard

Actual behavior:

Type is not narrowed. Type is only narrowed if Left and Right are guaranteed to be different types (e.g., by adding a private foo?: void member to Left).

@decademoon
Copy link

Here's another distinction:

class A {
    value: number;
}

class B {
    value: number;
}

let v: A | B;

if (v instanceof A) {
    // v is A
}
class A<T> {
    value: T;
}

class B<T> {
    value: T;
}

let v: A<number> | B<number>;

if (v instanceof A) {
    // v is A<number> | B<number>
}

Aren't these two examples exactly the same? Why would v in the second example not be deduced to A<number>?

@kitsonk
Copy link
Contributor

kitsonk commented Nov 8, 2016

This is a #202 issue... Until there is some form of nominal typing in TypeScript, types that are assignable to each other cannot be narrowed with the instanceof operator. It is also covered in TypeScript FAQ - Why Doesn't x instanceof Foo narrow x to Foo?

@crobi
Copy link
Author

crobi commented Nov 9, 2016

types that are assignable to each other cannot be narrowed with the instanceof operator.

I don't think this is the issue here. Why would the compiler think Left<number> is assignable to Right<string> in the original example? Those evaluate to {value: number} and {value: string}, respectively, and are definitely not assignable to each other.

It is also covered in TypeScript FAQ - Why Doesn't x instanceof Foo narrow x to Foo?

I don't see the types that are assignable to each other cannot be narrowed in that FAQ.

However, what I see is types that are not even compatible are not narrowed - does the compiler think that Left is incompatible with Left<number>?

@kitsonk
Copy link
Contributor

kitsonk commented Nov 9, 2016

The problem is that the instanceof operator does not allow a specification of a generic argument. So it has to assume Left<any>. Left<any> is assignable to Right<any>, therefore both Left<number> and Right<string> are assignable to Left<any>, therefore no narrowing. Again #202. Until there is some form of nominal concept of typing, these sorts of constructs won't work.

A workaround would be to use a custom type guard:

 class Left<T> {
    // private foo?: void;
    constructor(public readonly value: T) {};
  }

  class Right<T> {
    constructor(public readonly value: T) {};
  }

  function eitherNumberOrString(): Left<number> | Right<string> {
    return new Left(0);
  }

  // e is Left<number> | Right<string>
  const e = eitherNumberOrString();

  function isLeft<T>(value: any): value is Left<T> {
    return value instanceof Left;
  }

  if (isLeft<number>(e)) {
    e.value; // e.value is number
  }

@crobi
Copy link
Author

crobi commented Nov 9, 2016

the instanceof operator does not allow a specification of a generic argument. So it has to assume Left

Thanks for the explanation, that bit wasn't obvious to me. Especially since if the type guard succeeds, it narrows the type to the correct generic argument and not any:

class Value<T> {
    value: T;
}

class Person {
    name: string;
}

let x: Value<string> | Person;

// instanceof assumes Value<any>?
if (x instanceof Value) {
    // Narrowed to Value<string>, not Value<any>
    x.value;
}`

I assume this is expected because the type guard does something like intersecting Value<string> | Person with Value<any>? In any case, closing because of #202.

@crobi crobi closed this as completed Nov 9, 2016
@kitsonk
Copy link
Contributor

kitsonk commented Nov 9, 2016

I didn't notice at first your assignment to the generics. I thought they were the same values. When I looked at it again, I realised where my first explanation fell down

I assume this is expected because the type guard does something like intersecting Value<string> | Person with Value<any>?

It is assignability, Person cannot be assigned to Value<any>, therefore, Person can be eliminated from the type. If you had something like:

class Value { }

Which has no methods or properties, TypeScript assumes that is {} and then BOOM everything can be assigned to it and then you can't narrow those types. (Note, even if you included generic slots, if they aren't actually used in the definition, TypeScript eliminates them when doing comparability, which is why I didn't include a generic in the above example).

There is one side note in this, which would be a seperate feature request though... the construct of x instanceof Value<string> could be allowable, in my opinion, because the generics could be erased and that would narrow the comparison type, and while it still isn't a nominal comparison, it would solve this use cases and potentially others. Right now, when you pass generics, it evaluates the right hand as a constructor (typeof Value<string>) which is a TypeScript syntactical error, but wouldn't have to be.

@crobi
Copy link
Author

crobi commented Nov 9, 2016

the construct of x instanceof Value string could be allowable, in my opinion

I'm not sure this would work, both Value<string> and Value<number> have the same constructor and cannot be distinguished with the instanceof operator:

class Value<T> { value: T }

let x: Value<string> | Value<number>;

if (x instanceof Value<string>) {
  // Compiler thinks x is Value<string>
  // Runtime will enter this branch for Value<number> as well
}

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants