You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Element access with non-literal constant key should be narrowable #36230
TypeScript Version: 3.7.4 (also tried vNightly on the playground)
Search Terms: exhaustive type check array
Code
Error ts2532:
typeProgress=Array<number|undefined>|undefined;constprogress: Progress=[1,2,3];constindex=1;if(progress!==undefined&&progress[index]!==undefined&&progress[index]>=0){// Do something}
Workaround, but undesirable:
typeProgress=Array<number|undefined>|undefined;constprogress: Progress=[1,2,3];constindex=1;if(progress!==undefined)constp=progress[index];if(p!==undefined&&p>=0){// Do something}}
Expected behavior:
The exhaustive type checking mechanism in Typescript should know that neither progress nor progress[index] are undefined inside the if statement, because I'm doing an explicit check for that.
Actual behavior:
On progress[index] >= 0 you'll get ts2532: Object is possibly 'undefined'
Related Issues:
In #34661 the workaround seemed to be to use an intermediate variable, which will fix my code as well, while also making it more hairy.
If I recall correctly, Nathan Shively-Sanders (@sandersn) implemented control flow like this for literal element access, so at first glance it seems like it could be expanded to constant element access, but I’m not an expert in this area.
Me neither 😀
Literal element access is probably the right keyword. Because if index were a function and I'd be doing progress[index()] (hence it wouldn't be literal element access anymore) the error would be completely correct and understandable. TS wouldn't be able to know (with confidence) if the function returns the same result every time. But as for a constant value, that's fully predictable, if I understand const correctly.
I recall the cost of control flow analysis being much higher since there are many more element accesses with non-literal types than with literal types. But I could be wrong. It's worth making an experimental PR to see what the performance cost is.
Inserting flow nodes only for element accesses with literal access expressions was a performance decision from quite some time ago. As far as I know, we have no reason to revisit that decision.
I made that decision two years ago, so maybe something has changed since then. It's more likely that I was simply wrong and the performance hit is small, or worth it to get this feature.
changed the title [-]Element access should narrow with non-literal types too[/-][+]Element access with non-constant key should be narrowable[/+]on Feb 21, 2020
A particularly compelling case of this is when using const enum properties for discriminated union. Using the same property from the declaration doesn't narrow the type but the number literal does, even though both emit identical javascript code (Playground Link)
constenumX{key}interfaceA{[X.key]: "A";data_a: number}interfaceB{[X.key]: "B";data_b: number}interfaceC{[X.key]: "C";data_c: number}// doesn't workfunctiongetData_WITH_CONST_FROM_DECLARATION(thing: A|B|C){switch(thing[X.key]){case"A": returnthing.data_a;case"B": returnthing.data_b;case"C": returnthing.data_c;}}// works for only string or number literalsfunctiongetData_WITH_LITERAL(thing: A|B|C){switch(thing[0]){case"A": returnthing.data_a;case"B": returnthing.data_b;case"C": returnthing.data_c;}}
Also the title should probably be "allow constant but non literal key to be narrowable", non constant keys probably shouldn't work.
If I'm understanding correctly, the inability to narrow using constant keys is also a significant limitation of symbol properties. Thesecomments from #1863 suggest that using symbols as properties is fully supported, but code such as the following doesn't work:
constParent: unique symbol=Symbol('Parent');constName: unique symbol=Symbol('Name');interfaceGenericGroup{[Name]?: string;}interfaceCompanyextendsGenericGroup{}interfaceDivisionextendsGenericGroup{[Parent]?: Company;}interfaceTeamextendsGenericGroup{[Parent]?: Division;}typeGroup=Company|Division|Team;functiongetName(group: GenericGroup): string{if(group[Name]==null){return'';}// Error - "Type 'string | undefined' is not assignable to type 'string'"returngroup[Name];}functiongetParentName(group: Group): string|undefined{if(!(Parentingroup)){returnundefined;}// Error - "Element implicitly has an 'any' type because expression of type // 'unique symbol' can't be used to index type 'Group'."returngroup[Parent]?.[Name];}
Should the following behavior fall under the current issue or should it be its own?:
constrecord: Record<string,number|string>={};constkey=4;constconstKey=4asconst;letstringValue: string;record[4]='hello';stringValue=record[4];// @ts-expect-error - should be error because checker doesn't know what `key` is specificallyrecord[key]='hello';stringValue=record[key];record[constKey]='hello';// Proposal: allow `record[constKey]` to be narrowed since `constKey` is a `const` keystringValue=record[constKey];// Bonus: hopefully implementing above would make the following work for freeif(key===4){record[key]='hello';stringValue=record[key];}
changed the title [-]Element access with non-constant key should be narrowable[/-][+]Element access with non-literal key should be narrowable[/+]on Sep 3, 2020
changed the title [-]Element access with non-literal key should be narrowable[/-][+]Element access with non-literal constant key should be narrowable[/+]on Sep 3, 2020
Matt Broadstone (@mbroadst) yes that is the same issue, and foo[kSymbol] === 4 is still a valid statement if foo[kSymbol] is undefined so there is no type error, trying to do foo[kSymbol] === [] for array or foo[kSymbol].toFixed() for a number shows it.
Activity
andrewbranch commentedon Feb 14, 2020
If I recall correctly, Nathan Shively-Sanders (@sandersn) implemented control flow like this for literal element access, so at first glance it seems like it could be expanded to constant element access, but I’m not an expert in this area.
thany commentedon Feb 14, 2020
Me neither 😀
Literal element access is probably the right keyword. Because if
indexwere a function and I'd be doingprogress[index()](hence it wouldn't be literal element access anymore) the error would be completely correct and understandable. TS wouldn't be able to know (with confidence) if the function returns the same result every time. But as for a constant value, that's fully predictable, if I understandconstcorrectly.sandersn commentedon Feb 14, 2020
I recall the cost of control flow analysis being much higher since there are many more element accesses with non-literal types than with literal types. But I could be wrong. It's worth making an experimental PR to see what the performance cost is.
[-]Exhaustive typing & array elements[/-][+]Element access should narrow with non-literal types too[/+]DanielRosenwasser commentedon Feb 15, 2020
Didn't we talk about this at #32357 and #31478 ?
sandersn commentedon Feb 19, 2020
Yes:
I made that decision two years ago, so maybe something has changed since then. It's more likely that I was simply wrong and the performance hit is small, or worth it to get this feature.
[-]Element access should narrow with non-literal types too[/-][+]Element access with non-constant key should be narrowable[/+]tadhgmister commentedon Feb 21, 2020
A particularly compelling case of this is when using
const enumproperties for discriminated union. Using the same property from the declaration doesn't narrow the type but the number literal does, even though both emit identical javascript code (Playground Link)Also the title should probably be "allow constant but non literal key to be narrowable", non constant keys probably shouldn't work.
joshkel commentedon Mar 29, 2020
If I'm understanding correctly, the inability to narrow using constant keys is also a significant limitation of symbol properties. These comments from #1863 suggest that using symbols as properties is fully supported, but code such as the following doesn't work:
lazytype commentedon Apr 7, 2020
Should the following behavior fall under the current issue or should it be its own?:
Playground
sandersn commentedon Apr 8, 2020
lazytype it's the same;
const index = 1from the OP behaves the same as yourconst constKey = 4 as constin this case.[-]Element access with non-constant key should be narrowable[/-][+]Element access with non-literal key should be narrowable[/+][-]Element access with non-literal key should be narrowable[/-][+]Element access with non-literal constant key should be narrowable[/+]mbroadst commentedon Sep 28, 2020
Hi, I think I'm running into this bug please let me know if I should post elsewhere:
Curiously if the member variable is a primitive type this works just fine:
andrewbranch commentedon Sep 28, 2020
Yep, that’s this. The second example works because you’re always allowed to compare
foo[kSymbol]for equality even if it might be undefined.tadhgmister commentedon Sep 28, 2020
Matt Broadstone (@mbroadst) yes that is the same issue, and
foo[kSymbol] === 4is still a valid statement iffoo[kSymbol]is undefined so there is no type error, trying to dofoo[kSymbol] === []for array orfoo[kSymbol].toFixed()for a number shows it.