Access optional keys in object, where key is a literal in a literal union type without error "Object is possibly 'undefined'"

Viewed 517

Let's say I have a type Fields

export type NodeId =
    | "a-index"
    | "b-index"
    | "c-index"

export type Fields = {
    [id in NodeId]?: {
        [sectionId: string]: {
            [name: string]: string;
        };
    }
};

Why does the first set error with Object is possibly 'undefined', but not the second set?

const test: Fields = {};

const id = "a-index";

// Why does this error
let resultA;
if (test[id]) {
  resultA = test[id]["somesection"]
            ^^^^^^^^
            error TS2532: Object is possibly 'undefined'.
}

// But this works fine?
let resultB;
if (test["a-index"]) {
  resultB = test["a-index"]["somesection"]
}

The only difference being that I assigned id to a variable. Even explicitly typing id as NodeId doesn't satisfy typescript.

See typescript playground here

1 Answers

This is a known issue, microsoft/TypeScript#10530. The problem is that the compiler only spends time narrowing the type of an object property if the property access is direct, either by using a dotted identifier or by using bracket index with a string literal. So your resultB version works:

let resultB;
if (test["a-index"]) {
    resultB = test["a-index"]["somesection"]
}

But the resultA version doesn't because id, even though it is a const whose type is the literal "a-index", is a variable and the compiler doesn't do narrowing for it. According to microsoft/TypeScript#10565, an initial attempt to address this, adding this functionality significantly worsens the compiler perfomance. I guess having to check every possible indexing-into-an-object is more expensive than the potential control flow narrowing saves you.

The workaround here, in the case where you can't just replace id with a string literal, is to do the property access once and save its result to a new variable. Then type guards should work on this variable as expected:

let resultA;
const testId = test[id];
if (testId) {
    resultA = testId["somesection"]
}

Playground link to code

Related