Typescript: Discriminant property type guard not applied with bracket notation

Created on 25 Aug 2016  ·  14Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 2.0.0

Code

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    // In the following switch statement, the type of s is narrowed in each case clause
    // according to the value of the discriminant property, thus allowing the other properties
    // of that variant to be accessed without a type assertion.
    switch (s['kind']) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
    }
}

Expected behavior:

The code compiles without errors.

Actual behavior:

sample.ts(24,33): error TS2339: Property 'size' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(24,42): error TS2339: Property 'size' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(25,36): error TS2339: Property 'width' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(25,46): error TS2339: Property 'height' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(26,43): error TS2339: Property 'radius' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(26,54): error TS2339: Property 'radius' does not exist on type 'Square | Rectangle | Circle'.

Why this is bad:

I am trying to work with Dropbox's new 2.0 SDK, which _heavily_ uses tagged union types (especially for API errors). The discriminant property is named .tag, so it can only be accessed via bracket notation. I generated TypeScript typings for their new JavaScript SDK, and discovered this bug the hard way. :(

Bug Revisit

Most helpful comment

Fix is up at #10565

All 14 comments

:+1: Bracketed property access should work the same as dotted property access for the purposes of type guards when the indexed name matches a known property

Fix is up at #10565

Update from the PR at #10530: narrowing on element access, even just for strings and not numbers, adds 6.5% time to the checker, so this is not likely to go into 2.1 unless we come up with a way to reduce its cost.

@sandersn thanks for the update. That's unfortunate, but understandable. :(

... when the indexed name matches a known property.

Then I can rest assured that we can still continue to use unmatched index names?
So we can still hack the type by adding new properties using the [] syntax, right? :fearful:

@sandersn Can something else be done about this ? It is a really annoying issue which is evident from the large number of referenced bugs.

Does still adds only 6.5% at checking? Cause, I know it's a lot, but this is a must have when working in strict mode. It wouldn't compile the code otherwise.

This also negatively affects usability of the new optional tuple elements:

function foo(bar: [number, string, Function?]) {
    bar[2] && bar[2](); // Cannot invoke an object which is possibly 'undefined'.
}

Keywords: narrow narrowing element access index indexer elementaccessexpression cfa control flow analysis

I'm having an issue that I _think_ is related to this, but i'm not certain but I don't want to open a new bug report if it is.

Todo is a subclass of Note.Todo contains a toggleComplete() method and Note does not.

let notes:Note[] = [new Todo("todo")];
if (notes[0] instanceof Todo) {
   notes[0].toggleComplete(); 
   //should work. Instead, gives an error that toggleComplete does not exist on type Note.
}

It works fine if I assign notes[0] to a variable first with let n = notes[0] but that's an annoying workaround that doesn't transition well to indexed for loops (I'm teaching new programmers and don't want to add for..of to the mix).

Is this issue the same as this simple problem? I was told on stackoverflow that it was, but it feels like a simpler problem than the one being discussed here and in the various duplicates, since my issue is limited to arrays with numeric literal indices.

TS doesn't narrow the type if I use a variable as index instead of a string literal, even if the variable has the same type as the string literal (e.g., both have the type of destination.address). Here's a REPL: https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=21&pc=2#code/MYewdgzgLgBAIgUQMoBUCSA5Agug8hgfSzjgCVkkYBeGAcgBMBTaASzAEMoXwA6d++gCdmEWgG4AUKEiwkCUgDU0AYQQFsAWQTU6ERoIBuLYIx4cAto3ESJUAJ4AHRjAwgmAIXZ6dAbxgt6AC4YaEE2AHMYAF9JeycYNDAofQ4AG1cmHQzGT28AMhg-AG05RRU1TQQAXWDQiOjYx2cEAA9kwTTsrLccr2cC4sRUTBw0fCISciQkGpCoMLBImNsmmGVwMEZgLnAumkT2zp6YAB8YVsP2dJ7JKXBoGHDGKDR6HQAKMB7g9bBN7e4YGyAEpChIYBD-AAzGDvIbobB4QjEMgUfxgGBfJignzgyH44RQACuHUxPSK8JGSImqOmVUk+KieIhhJJGKxjBK8iUqnUWC09IkTLuMkez1eACYPhyfhstjsgT0cczobCGMwuBwFXwBMIIKJ0WTsWD8QTnmyjZzKYixsjJhRBYyVazSRyuWVeZVBVEgA

I also found out that variables in bracket notation seem to not work in type guards. Very minimal reproduction:

const obj: { prop: string | null }  = { prop: "hello" };

if (typeof obj.prop === "string")
    obj.prop.length; //OK

if (typeof obj["prop"] === "string")
    obj["prop"].length; //OK

const key = "prop" as const;
if (typeof obj[key] === "string")
    obj[key].length; //error - object is possibly 'null'

Playground Link

It seems like using hardcoded values in bracket notation works, which is a fix made in https://github.com/microsoft/TypeScript/issues/28081 however, using a variable doesn't offer the same behaviour, even if that variable is a const. So, it seems inconsistent. I'd understand if the variable was mutable or maybe even an object (perhaps its toString() doesn't return the same thing every time) but for an immutable string variable, this should work the same as a hard-coded value.

I have a very similar question, in the following example lookup[n] doesn't narrow, but a const P = lookup[n] does, with no intervening yield/await/call side-effects.

Is this the same issue?

declare const lookup: { [P in string]?: string  }

function f(n: string) {
    const P = lookup[n] ;   // typeof P = string | undefined (correct)
    if (lookup[n]) {
        const Q = lookup[n] // typeof Q = string | undefined (huh?)
    }
    if (P) {
        const R = P ;       // typeof R = string (correct)
    }
}

I note from other issues that the case where the expression in the square-bracket is a literal has been fixed (eg https://github.com/microsoft/TypeScript/pull/31478), but this is not the same case. This is the same as @PurpleMagick 's above: the case where the expression within the brackets is invariant between accesses. This will be true in JS as long as there is no intervening yield, await or function call and all the accesses are to locally defined variables (let, const, var, parameter)

The reason is for this request is the very common "test and access" case

I'm also running into this while trying to safely access Redux Reducers via their type property.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kyasbal-1994 picture kyasbal-1994  ·  3Comments

siddjain picture siddjain  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

CyrusNajmabadi picture CyrusNajmabadi  ·  3Comments

fwanicka picture fwanicka  ·  3Comments