Type narrowing breaks when accessing an object property with bracket notation
Asked Answered
E

3

6

Why do tests 1 and 2 work here, but test 3 shows a compiler error at foo[barConst]++: 'Object is possibly "undefined".'? I often need to access properties via bracket notation and thus like to have constants for these properties, but TypeScript doesn't allow this. It also doesn't work with const enums. Is it a bug or is there a good reason for the error?

const barConst = 'bar';

interface Foo {
    [barConst]?: number;
}

function test1(foo?: Foo) {
    if (foo && foo.bar) {
        foo.bar++;
    }
}

function test2(foo?: Foo) {
    if (foo && foo['bar']) {
        foo['bar']++;
    }
}

function test3(foo?: Foo) {
    if (foo && foo[barConst]) {
        foo[barConst]++; // compiler error: 'Object is possibly "undefined".'
    }
}

Playground

Exchequer answered 11/9, 2019 at 11:24 Comment(4)
I just copied your code, and it works correctly. check if in your tsconfig file strict option is true or false. maybe, this is a problemCrabby
Can confirm, it works with strictNullChecks disabled. Why, though? P.S. disabling this setting is not an option for me.Exchequer
foo[barConst] inside test3 will be evaluated in runtime. just because is inside a function (easy explanation). interface Foo is not known for type checking when you are passing into test3 function. for test1 and test2, you explicitly checked for a value of the input parameter foo, so compiler does not complain. If you update you question, what is the use case for this, maybe I can help you find another solution.Crabby
Are you looking for dynamic keys for the Foo interface?Crabby
A
2

Narrowing the property access via computed propertyNames/literal expressions seems to be not possible currently. Have a look at this issue and its PR, also that issue.

You can narrow property access in bracket notation with string literals like for example foo["bar"]. Dynamic expressions like foo[barConst] don't work. Assigning foo[barConst] to a variable and working/narrowing down this variable instead is an alternative, but costs an additional declaration.

In your case the simplest solution would be to just cast the expression with non-null assertion operator !. As you do a pre-check for a falsy value, you are safe here:

function test3(foo?: Foo) {
    if (foo && foo[barConst]) {
        foo[barConst]!++; 
    }
}
Amaryl answered 11/9, 2019 at 14:25 Comment(0)
G
3

With the release of TypeScript 4.7 due to its improvements to the control flow analysis for bracketed element access, the original code no longer results in a compiler error because the foo && foo[barConst] guard now correctly narrows the foo[barConst] access to be of type number.

See PR #40617 for more details on the CFA update overall.

4.7 Playground

Galligan answered 26/5, 2022 at 0:41 Comment(0)
A
2

Narrowing the property access via computed propertyNames/literal expressions seems to be not possible currently. Have a look at this issue and its PR, also that issue.

You can narrow property access in bracket notation with string literals like for example foo["bar"]. Dynamic expressions like foo[barConst] don't work. Assigning foo[barConst] to a variable and working/narrowing down this variable instead is an alternative, but costs an additional declaration.

In your case the simplest solution would be to just cast the expression with non-null assertion operator !. As you do a pre-check for a falsy value, you are safe here:

function test3(foo?: Foo) {
    if (foo && foo[barConst]) {
        foo[barConst]!++; 
    }
}
Amaryl answered 11/9, 2019 at 14:25 Comment(0)
C
0

Continue of comment discussions. (I still do not know what are you looking for) you can try something like this:

type barConst = 'bar';

type Foo<K extends string> = Record<K, number>;

function test3(foo?: Foo<barConst>) {
  if (typeof foo !== 'undefined') {
    foo['bar']++;
  }
}
let x: Foo<barConst> = { bar: 2 };

test3(x);
console.log(x);

You can have autocomplete and no compiler error. Still, please Update your question.

Crabby answered 11/9, 2019 at 13:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.