Typescript: Union not narrowed with `typeof x.y` as a discriminant

Created on 15 Jul 2019  路  7Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.6.0-dev.20190713


Search Terms: TS2339, union type object, union type property does not exist
And github suggestions while I was writing the title.

Code

type Test = {
    someKey: false
} | {
    someKey: {
        someKeyInside?: number
    },
    someKeyThatExistOnlyIfOtherKeyIsNotFalse: string;
};

declare const test: Test;

if (typeof test.someKey === 'object') {
    console.log(test.someKeyThatExistOnlyIfOtherKeyIsNotFalse);
}

Expected behavior:
String in someKeyThatExistOnlyIfOtherKeyIsNotFalse should be logged

Actual behavior:

error TS2339: Property 'someKeyThatExistOnlyIfSomeKeyIsNotFalse' does not exist on type 'Test2'.
  Property 'someKeyThatExistOnlyIfSomeKeyIsNotFalse' does not exist on type '{ someKey: false; }'.

29     console.log(test2.someKeyThatExistOnlyIfSomeKeyIsNotFalse); // TS2339: Property 'someKeyThatExistOnlyIfSomeKeyIsNotFalse' does not exist on type '{ someKey: false; }'.

Playground Link:
https://www.typescriptlang.org/play/index.html#code/C4TwDgpgBAKhDOwCMUC8UDeAoKurwHsBbCAaQhAC4oAzAQwBt4IsBfKAH0xz0JPKrc8w-MTIUAkgDt4ASwAmEalICuRAEYQATj1ysANLtH8KMABZ1gAUQAesxAHkpDEBJoPgZ7QInwAcgTAAGKMzNSIWrJSAOYA3GzxWIoAxgx0WtDJBDLAUMAIyNRwiEiJsjRQABT5JQB0fOIgAJRCeFkyBAwQtQwE0dUFSPViAuaWtvbATi5uHl5aPv6BIUwQTbFQAPSbUA6kAIRsWFigkLAFAExorbgNAtT0q2ycN8aN1NgieNuwXlDZLn+Umg8nKNG0ECkyWgNC0xDy4GgxWQ+jePhkCkydCkUE0UBUUkUNCiEHkQLRFCMwjukgxigA-Mo1JodMIDEYaSAxtY7I5nK53J5vJIlsFQkp8MBIjF4qxEik0hkoO1EHlLkVLmUKtVEQQKjVgBdhiYQGhUOgAOQEdQAKwgyWAFpanza2UIXR6fQGiCNnO5Ez5M0F80WATFq3WWx2MAAyhcAMzxgCc1AACnDIFpQFALX6LDzJtMBXNha5RStmBaoPICAgoFJAlAILzctkEWcLRgKYJHswNqwLbU2EA

Take a look at type Test1. The only one difference from type Test2 is someKeyInside should be always defined.

Related Issues:
-

Bug Moderate help wanted

Most helpful comment

Just chiming in to say I would love this fixed! Our repro is something like the following:

interface A { x: string, y: string };
interface B { x: number, y: number };
type X = A | B;

declare var bar: X;

if (typeof bar.x === 'string') {
    let y = bar.y; // string | number, but really should be string.
}

Essentially we have some methods we'd like to return union of object types from. The object types are mostly distinct with a little overlap. Ideally users would just check the field that they want is present, and TS would do the math and present a proper type inside the if block.

We ran in to this because we were using a tagged union pattern which was working fine, but then attempted to generalize it, and found the narrowing stopped working.

All 7 comments

I'm not sure typeof has ever triggered narrowing by discriminant.

Just chiming in to say I would love this fixed! Our repro is something like the following:

interface A { x: string, y: string };
interface B { x: number, y: number };
type X = A | B;

declare var bar: X;

if (typeof bar.x === 'string') {
    let y = bar.y; // string | number, but really should be string.
}

Essentially we have some methods we'd like to return union of object types from. The object types are mostly distinct with a little overlap. Ideally users would just check the field that they want is present, and TS would do the math and present a proper type inside the if block.

We ran in to this because we were using a tagged union pattern which was working fine, but then attempted to generalize it, and found the narrowing stopped working.

Looks interesting. I would have a try on this.

Hi, if I define

interface Foo1 {
    key: number|undefined;
    f1: number;
}

interface Foo2 {
    key: string|undefined;
    f2: number;
}

type U = Foo1 | Foo2

function f1(u: U) {
    if (typeof u.key !== 'number' && typeof u.key !== 'undefined') {
        u;       // What is this?
        u.key; // What is this? 
    }
}

What type of u and u.key should be?
Should it be Foo2? If so, could some guy give me some advice about how to change the flow?

Maybe it should have comparable behavior to the literal type case?

interface Foo1 {
    key: "number" | "undefined";
    f1: number;
}

interface Foo2 {
    key: "string" | "undefined";
    f2: number;
}

type U = Foo1 | Foo2

function f1(u: U) {
    if (u.key !== 'number' && u.key !== 'undefined') {
        u;       // U
        u.key;   // 'string'
    }
}

I ran across this after seeing @jack-williams's comment here: https://github.com/microsoft/TypeScript/issues/30622#issuecomment-477398183

I was trying to "entangle" the refinements on two types by sticking them in an object together and was surprised that it didn't work:

function f(x: string | number, y: string | number) {
  let o;
  if (typeof x === 'string' && typeof y === 'string') {
    o = { x, y };
  } else if (typeof x === 'number' && typeof y === 'number') {
    o = { x, y };
  } else {
    throw new Error(`Can't have mixed types: ${typeof x} and ${typeof y}`);
  }

  o;  // type is { x: string; y: string; } |
      //         { x: number; y: number; }
  o.x;  // type is string | number
  if (typeof o.x === 'string') {
    o.x;  // type is string
    o;  // type is the same as above
    o.y;  // type is string | number :(
  }
}

([playground])

This would be useful to have since the improved tuple types in 4.0 will make overload implementations that use tuples even more common.

function Example(a: number, b?: number): number;
function Example(a: string, b?: string): string;
function Example(...args: [a: number, b?: number] | [a: string, b?: string]): number | string {
  if (typeof args[0] === 'number') {
    // doesn't narrow
  } else {
    // doesn't narrow
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments

manekinekko picture manekinekko  路  3Comments

blendsdk picture blendsdk  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments