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
narrow object by property
(unfortunately, this returned way too many results to look through, so sorry if this is a dupe!)
Suggestion
Narrow objects based on checks of their properties.
Use Cases
Some function receives an object as input and then does validation on that object prior to passing it on to some other function, or returning it as a narrowed object.
Examples
declareconstapple: {readonlyprop: number}if(apple.prop!==2)thrownewError()constappleProp: 2=apple.prop// no error// Type 'Apple' is not assignable to type '{ prop: 2; }'.// Types of property 'prop' are incompatible.// Type 'number' is not assignable to type '2'.constbanana: {prop:2}=apple
In this example, we can see that the compiler is tracking the narrowed type of apple.prop because it allows us to assign it to a variable of type 2. However, when we try to assign the containing object to something that wants a narrowed prop we get a type error.
Checklist
My suggestion meets these guidelines:
This wouldn't be a breaking change in existing TypeScript/JavaScript code
This wouldn't change the runtime behavior of existing JavaScript code
This could be implemented without emitting different JS based on the types of the expressions
This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
I updated original to make apple.prop readonly. Though, your example does show why this may be harder to implement than I original thought, since it needs to only narrow for readonly properties.
I understand your pain because, in some cases, you know that apple's props won't be modified for the duration that banana is in scope. So, at that point, the assignment to banana is sound.
AnyhowStep Is the fact that you can assign writable to apple in your example a bug? It feels like a bug since you are narrowing writable without any explicit assertion or type guard.
Perhaps more surprising is that { readonly prop : number } is assignable to { prop : number } (if memory serves, I'm not on the computer)
IIRC this is historical. readonly was a later addition to the language and there’s not yet a writeable annotation, so for compatibility with existing code, it can’t disallow the assignment because there was (at one time) no way to distinguish read-only from writable.
I’m reasonably sure most of the unsoundness surrounding readonly traces to the above historical note.
Micah Zoltu (@MicahZoltu) can you give an example that doesn't involve numbers? Right now it sounds like you want number to be treated as the union of all numbers. This is not feasible in our current system but might be possible by introducing numeric ranges into the type system.
I also do not understand the intent of disallowing
typeof apple ↠ { prop: 2 }
but allowing
(typeof apple)['prop'] ↠ 2.
Did the second example predate the addition of readonly?
Nathan Shively-Sanders (@sandersn) From the discussion on Gitter that led to this issue, the main point of contention was that apple.prop itself gets narrowed to 2 but typeof apple doesn’t change. So depending on how you use apple after narrowing, you’re in an odd state where prop is simultaneously narrowed to 2 or not, depending if you rely on the narrowing directly or indirectly.
What Micah Zoltu (@MicahZoltu) wanted, essentially, was for typeof apple to become Apple & { prop: 2 } after the typeguard.
declareconstapple: {readonlyprop: number}if(apple.prop!==2)thrownewError()constappleProp: 2=apple.prop// no error// Type '{ readonly prop: number; }' is not assignable to type '{ readonly prop: 2; }'.// Types of property 'prop' are incompatible.// Type 'number' is not assignable to type '2'.constbanana: {readonlyprop:2}=apple// errorconstcherry: {readonlyprop:2}={prop: apple.prop}
The assignment to cherry shows that clearly TS knows that apple.prop is of type 2, not number, yet when I try to assign the object itself it doesn't agree with that. I suspect this is because of the reasons that AnyhowStep went into above in that readonly doesn't actually mean readonly in TypeScript, which makes my blood boil but at least explains the oddity here.
Reading the issues linked above, a comment from Jack Williams (@jack-williams) in one of them indicates this is a design constraint:
Type guards do not propagate type narrowings to parent objects. The narrowing is only applied upon access of the narrowed property which is why the destructing function works, but the reference function does not. Narrowing the parent would involve synthesizing new types which would be expensive.
Also, it would seem (based on AnyhowStep's commentary above) that TypeScript thinks that while apple.prop is currently2, there is no guarantee that it will remain 2 indefinitely. This is because readonly appears to not mean what one thinks it means, and in fact apple.prop can be assigned any number later on, so TS cannot narrow apple.prop to 2.
So IIUC, the current behavior is a design constraint, and AnyhowStep thinks that even if it wasn't, it should not be fixed because readonly is a lie.
Micah Zoltu (@MicahZoltu) Oh, I misunderstood your idea, and also didn't know that we already narrow numbers to numeric literals. Anyway, as Jack Williams (@jack-williams) says, this is exactly a duplicate of previous issues. And since it would require the compiler to synthesise new types, we haven't done it because of the performance implications.
For future readers, this is a duplicate of #31755, which is a duplicate of #30506, which is closed as a Design Limitation (though it is missing a tag).
The crux of the problem is:
Type guards do not propagate type narrowings to parent objects. The narrowing is only applied upon access of the narrowed property which is why the destructing function works, but the reference function does not.
The reason this is a Design Limitation is because:
Narrowing the parent would involve synthesizing new types which would be expensive.
Activity
AnyhowStep commentedon Sep 3, 2019
I am against this being allowed.
MicahZoltu commentedon Sep 3, 2019
I updated original to make
apple.propreadonly. Though, your example does show why this may be harder to implement than I original thought, since it needs to only narrow for readonly properties.AnyhowStep commentedon Sep 3, 2019
Copy-pasting from Gitter,
I understand your pain because, in some cases, you know that
apple's props won't be modified for the duration thatbananais in scope. So, at that point, the assignment tobananais sound.AnyhowStep commentedon Sep 3, 2019
Also,
readonlyisn't a guarantee that the property won't be modified, even if TS started handling readonly "properly".We can't modify
apple.propbecause it isreadonlybutwritable.propis fair game.MicahZoltu commentedon Sep 3, 2019
AnyhowStep Is the fact that you can assign
writabletoapplein your example a bug? It feels like a bug since you are narrowing writable without any explicit assertion or type guard.AnyhowStep commentedon Sep 3, 2019
It's not a bug.
{ prop : number }is assignable to{ readonly prop : number }The current meaning of
readonlyis,We are not saying,
If
readonlymeant the latter, then it would be a bug to allow the assignment fromwritabletoapple.Perhaps more surprising is that
{ readonly prop : number }is assignable to{ prop : number }(if memory serves, I'm not on the computer)But if we keep in mind what
readonlycurrently means, then it "makes sense".It does limit the usefulness of
readonly, in my opinion, butreadonlyis still useful, despite its shortcomingsfatcerberus commentedon Sep 3, 2019
IIRC this is historical.
readonlywas a later addition to the language and there’s not yet awriteableannotation, so for compatibility with existing code, it can’t disallow the assignment because there was (at one time) no way to distinguish read-only from writable.I’m reasonably sure most of the unsoundness surrounding
readonlytraces to the above historical note.jack-williams commentedon Sep 3, 2019
Related #29827, #30506, #31755.
sandersn commentedon Sep 3, 2019
Micah Zoltu (@MicahZoltu) can you give an example that doesn't involve numbers? Right now it sounds like you want
numberto be treated as the union of all numbers. This is not feasible in our current system but might be possible by introducing numeric ranges into the type system.I also do not understand the intent of disallowing
typeof apple↠{ prop: 2 }but allowing
(typeof apple)['prop']↠2.Did the second example predate the addition of readonly?
AnyhowStep commentedon Sep 3, 2019
typeof apple↠{ prop: 2 }should be disallowed because,#33205 (comment)
fatcerberus commentedon Sep 3, 2019
Nathan Shively-Sanders (@sandersn) From the discussion on Gitter that led to this issue, the main point of contention was that
apple.propitself gets narrowed to2buttypeof appledoesn’t change. So depending on how you useappleafter narrowing, you’re in an odd state wherepropis simultaneously narrowed to2or not, depending if you rely on the narrowing directly or indirectly.What Micah Zoltu (@MicahZoltu) wanted, essentially, was for
typeof appleto becomeApple & { prop: 2 }after the typeguard.MicahZoltu commentedon Sep 4, 2019
Bruce Pascoe (@fatcerberus) is correct. To help exemplify the problem:
The assignment to
cherryshows that clearly TS knows thatapple.propis of type2, notnumber, yet when I try to assign the object itself it doesn't agree with that. I suspect this is because of the reasons that AnyhowStep went into above in thatreadonlydoesn't actually meanreadonlyin TypeScript, which makes my blood boil but at least explains the oddity here.Reading the issues linked above, a comment from Jack Williams (@jack-williams) in one of them indicates this is a design constraint:
Also, it would seem (based on AnyhowStep's commentary above) that TypeScript thinks that while
apple.propis currently2, there is no guarantee that it will remain2indefinitely. This is becausereadonlyappears to not mean what one thinks it means, and in factapple.propcan be assigned any number later on, so TS cannot narrowapple.propto2.So IIUC, the current behavior is a design constraint, and AnyhowStep thinks that even if it wasn't, it should not be fixed because
readonlyis a lie.sandersn commentedon Sep 4, 2019
Micah Zoltu (@MicahZoltu) Oh, I misunderstood your idea, and also didn't know that we already narrow numbers to numeric literals. Anyway, as Jack Williams (@jack-williams) says, this is exactly a duplicate of previous issues. And since it would require the compiler to synthesise new types, we haven't done it because of the performance implications.
MicahZoltu commentedon Sep 5, 2019
For future readers, this is a duplicate of #31755, which is a duplicate of #30506, which is closed as a Design Limitation (though it is missing a tag).
The crux of the problem is:
The reason this is a Design Limitation is because:
typescript-bot commentedon Sep 7, 2019
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
typescript-bot commentedon Sep 10, 2019
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.