TypeScript: Unions without discriminant properties do not perform excess property checking
TypeScript Version: 2.7.0-dev.201xxxxx
Code
// An object to hold all the possible options
type AllOptions = {
startDate: Date
endDate: Date
author: number
}
// Any combination of startDate, endDate can be used
type DateOptions =
| Pick<AllOptions, 'startDate'>
| Pick<AllOptions, 'endDate'>
| Pick<AllOptions, 'startDate' | 'endDate'>
type AuthorOptions = Pick<AllOptions, 'author'>
type AllowedOptions = DateOptions | AuthorOptions
const options: AllowedOptions = {
startDate: new Date(),
author: 1
}
Expected behavior:
An error that options
cannot be coerced into type AllowedOptions
because startDate
and author
cannot appear in the same object.
Actual behavior:
It compiles fine.
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 45
- Comments: 33 (17 by maintainers)
Links to this issue
- typescript - Why does A | B allow a combination of both, and how can I prevent it? - Stack Overflow
- Why does TypeScript's Structural Typing (i.e. "Duck Typing") necessitate non-strict Type Unions? - Stack Overflow
- symbols - Typescript types allow unwanted properties - Stack Overflow
- TypeScript: type for an object with only one key (no union type allowed as a key) - Stack Overflow
@svatal I have a workaround for this in the form of
StrictUnion
. I also have a decent writeup of it here:Just thought I’d bring up another approach to “human-readability”,
Playground
If
InvalidKeys
is too verbose, you can make the name as short as an underscore, PlaygroundWith
Compute
,Playground
I like the
Compute
approach when the union is not “noisy” (the ratio of invalid to valid keys is closer to 0 or 0.5).I like the
InvalidKeys
approach when the union is “noisy” (the ratio of invalid to valid keys is closer to one or higher).On review of this, it seems like the scenario in the OP isn’t something we could feasibly touch at this point - it’s been the behavior of EPC for unions since either of those features existed, and people write quasioverlapping unions fairly often.
The solutions described above are all pretty good and honestly much more expressive than just the originally-proposed behavior change, so I’d recommend choosing one of those based on what you want to happen.
Overall Exact/Closed types seem like the real underlying request for most of these scenarios, so follow #12936 for next steps there.
Contextual typing is not relevant here. Neither is
Pick
. Here’s a smaller repro that avoids contextual typing:Excess property checking of unions does not work because all it does is try to find a single member of the union to check for excess properties. It does this based on discriminant properties, but
A
doesn’t have any overlapping properties, so it can’t possibly have any discriminant properties. (The same is true of the originalAllowedOptions
.) So excess property checking is just skipped. I chose this design because it was conservative enough to avoid bogus errors, if I remember correctly.To get excess property checks for unions without discriminant properties, we could just choose the first union member that has at least one property shared with the source, but that would behave badly with the original example:
Here, we’d choose
Pick<AllOptions, 'startDate'>
as the first matching union member and then markendDate
as an excess property.@sylvanaar @DanielRosenwasser Let me know if you have ideas for some other way to get excess property checks for unions without discriminant properties.
I think this might be another example:
You can also see in this screencapture that TypeScript suggests properties and combinations that shouldn’t be allowed:
https://github.com/microsoft/TypeScript/assets/7614039/1285dfdb-eb4a-4879-ba9b-5e6eaa7c0aaf
@SlurpTheo
This happens because we should never compute a function type by intersecting it with an object,
Compute
from the ts-toolbelt handles this scenario.I think I just ran into this issue with this snippet,
Playground
I know TS only performs excess property checks on object literals. But I also need to perform excess property checks when the destination type is a union.
Seems like it doesn’t do that at the moment.
@dragomirtitian 's workaround works but it does make the resulting type much uglier.
Another example:
Built-in type guards pretend that TS has excess property checking on unions, when really it doesn’t.
EDIT: I was fooled by comment-compression into thinking the type-guard case was not mentioned. Apologies for the repetition.
Wowsa, #12936 is quite the chain there 😺 After skimming I don’t immediately follow how it will help with providing something like a Strict Union (i.e., really don’t want to allow excess properties), but I have subscribed.
@AnyhowStep @pirix-gh I always use this type to expand out other types, not necessarily better just another option:
Also I tend to use it sparingly, if the union constituents are named, the expanded version is arguably worse IMO.
In addition to @dragomirtitian’s
StrictUnion
, I would just addCompute
to make it human-readable:Someone can confirm for me that this is a bug, it seems like a excess property checking bug, making it validate on the first property to match one seems like the logical thing to happen? If it is I can make a PR @sandersn @DanielRosenwasser ?