optional chain chaining required array object index access
The return type of any property that is accessed with the optional chaining operator should include undefined, even when the type of the value says the property is required/non-optional. This is arguably a better reflection of the runtime behaviour, and would be useful in cases where the type does not reflect reality, and it's hard for the consumer to change that type.
The main use case would be index access. Today, TypeScript will assume that array/object index access will return a non-undefined value. This is not an exact reflection of runtime behaviour, but it is a conscious decision. I'd like to use the optional chaining operator to have safe access at runtime, but I'd like the returned type to reflect the possibility that the accessed value can now be undefined.
type Hit = { value: number };
type ObjWithOptionalProp = {
key?: Hit
};
type ObjWithRequiredProp = {
[x: string]: Hit;
}
const optional: ObjWithOptionalProp = {};
const required: ObjWithRequiredProp = { key: { value: 1 } };
// number | undefined
const optionalValue = optional.key?.value;
// number, could cause runtime error
const requiredValue = required.key.value;
// number (should be number | undefined)
const safeAccessRequiredValue = required.key?.value;
const hits: Array<Hit> = [];
// number, could cause runtime error
const valueFromIndex = hits[0].value;
// number (should be number | undefined)
const safeAccessValueFromIndex = hits[0]?.value;
My suggestion meets these guidelines:
This is consistent with other operators like ?? and ||. See https://github.com/microsoft/TypeScript/issues/34886 (which was marked Working as Intended). Control flow analysis determines that the fallback case is unreachable (i.e. it trusts the types) and narrows it away.
@fatcerberus Right, I see what you mean. I still hope that this will be considered differently because TypeScript's approach to array and object indexing is kind of controversial. This would allow an opt-out of that approach that has both runtime and type safety (and also creates an incentive to fix erroneous types where possible).
Distantly related: #13778.
This is really just a straight duplicate of #13778 - the use of ?. is tangential
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
@RyanCavanaugh To me, the use of ?. is really significant, and makes this distinct from #13778. I confess to not having read the whole thread there, so my apologies if I've missed something, but the key distinction imo goes back to something you said here:
Think of the two types of keys in the world: Those which you know do have a corresponding property in some object (safe), those which you don't know to have a corresponding property in some object (dangerous)... You get the second kind from key, the "dangerous" kind, from things like user inputs, or random JSON files from disk, or some list of keys which may be present but might not be.
So if you have a key of the dangerous kind and index by it, it'd be nice to have
| undefinedin here. But the proposal [in #13778] isn't "Treat dangerous keys as dangerous", it's "Treat all keys, even safe ones, as dangerous". And once you start treating safe keys as dangerous, life really sucks.
From my POV, the author explicitly adding ?. to their property access is a natural way for them to indicate that the property they're accessing is a "dangerous key" (and it's what they'd want to do anyway to get proper runtime handling). So, as I'm thinking about it, having special | undefined for optional chaining accesses is good compromise way to get the benefits of #13778 without the drawbacks.
Including | undefined on ?. accesses also feels like it's more important for avoiding "wtf" moments. Like, getting a T when indexing into a T[] isn't that surprising; as a user, I just think "here's one of the many places where TS has sacrificed technical correctness for ergonomics". But not getting back | undefined when I've used a syntactic construct whose whole point is to handle the possibility of | undefined does feel much more "wtf".
Granted, special casing ?. probably means some hackier code in the compiler (not sure how much), so I can see the implementer POV by which it's convenient to think of this as a straight duplicate of #13778, but I do think they're decidedly different from a user POV.
@RyanCavanaugh Do you think there鈥檚 anything to the distinction I鈥檓 trying to draw above? If so, could this be reopened and no longer marked a duplicate of #13778?
This issue is a well-defined subset of #13778. Changing index signatures to add | undefined would resolve this issue, but this issue can also be resolved without changing index signatures globally. Whether this issue should be resolved separately or by changing index signatures globally is ultimately a matter of opinion.
And speaking of: In my view, that TypeScript's 'understanding' of the type of required.key?.value is (almost certainly wrongly) less conservative than the author's understanding of the type is an acute failure of the TS engine: TS code is less safe than JS code, and fundamentally ignores the type information the author is conveying.
This is only tangentially related to #13778, in that it would provide a potential workaround for parts of #13778. This issue has use cases completely unrelated to #13778 and should not be marked as a duplicate at all.
The main focus here is on the ?. operator. IMO, the ?. is conceptually the opposite of the non-null assertion (!). Whereas ! removes null | undefined from a type, ?. should add undefined to the type, regardless of whether the type was previously nullable. That makes sense to me, conceptually. @RyanCavanaugh can you reopen this and remove the duplicate tag, taking into account that this issue is not really a duplicate of #13778, which is more about the type of array/object accessing than about the behavior of the ?. operator?
@RyanCavanaugh I see you haven't responded here鈥攄o you have anything to add? How exactly is this a duplicate of #13778? This issue has nothing to do with index signatures while that issue has everything to do with them.
Most helpful comment
@RyanCavanaugh To me, the use of
?.is really significant, and makes this distinct from #13778. I confess to not having read the whole thread there, so my apologies if I've missed something, but the key distinction imo goes back to something you said here:From my POV, the author explicitly adding
?.to their property access is a natural way for them to indicate that the property they're accessing is a "dangerous key" (and it's what they'd want to do anyway to get proper runtime handling). So, as I'm thinking about it, having special| undefinedfor optional chaining accesses is good compromise way to get the benefits of #13778 without the drawbacks.Including
| undefinedon?.accesses also feels like it's more important for avoiding "wtf" moments. Like, getting aTwhen indexing into aT[]isn't that surprising; as a user, I just think "here's one of the many places where TS has sacrificed technical correctness for ergonomics". But not getting back| undefinedwhen I've used a syntactic construct whose whole point is to handle the possibility of| undefineddoes feel much more "wtf".Granted, special casing
?.probably means some hackier code in the compiler (not sure how much), so I can see the implementer POV by which it's convenient to think of this as a straight duplicate of #13778, but I do think they're decidedly different from a user POV.