Skip to content

When using Omit, (TypeA | TypeB) & {...} doesn't work as expected anymore #46220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
RowanDZ opened this issue Oct 5, 2021 · 5 comments
Closed

Comments

@RowanDZ
Copy link

RowanDZ commented Oct 5, 2021

Bug Report

🔎 Search Terms

Omit, OR

🕗 Version & Regression Information

I tried multiple versions on the playground, but all(?) of them have it.

If using Omit, an OR statement doesn't work as expected anymore.
(sorry, I don't know how to call the (TypeA | TypeB) part)

⏯ Playground Link

Playground link with relevant code

💻 Code

type AButNotB = { a: number; b?: never }
type BButNotA = { a?: never; b: number }

type Test = (AButNotB | BButNotA) & { c: number; d: number; e: number }

const test1: Omit<Test, "e"> = {
  a: 1, // INCORRECT: where is the error??? `a` and `b` are both defined
  b: 2,
  c: 3,
  d: 4,
}

const test2: Test = {
  a: 1, // CORRECT: gives error
  b: 2,
  c: 3,
  d: 4,
}

🙁 Actual behavior

No error for test1

🙂 Expected behavior

Error like test2

@MartinJohns
Copy link
Contributor

Omit uses keyof, which only returns common properties for union types. Works as expected.

@RowanDZ
Copy link
Author

RowanDZ commented Oct 5, 2021

Hmm, okay, is there any way to accomplish what I expect then?

@MartinJohns
Copy link
Contributor

MartinJohns commented Oct 5, 2021

Look up "distributive omit".

Besides that there simply is no good working solution to enforce the absence of a property in TypeScript. Trying to design your code like this is doomed to cause pain.

@MartinJohns
Copy link
Contributor

A bit of further explanation, because I'm bored right now:

... And while I wrote this I realized that keyof is not the culprit, at least not entirely.

The type Omit<T, K> is defined as: type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
And the type Pick<T, K> is defined as: type Pick<T, K extends keyof T> = { [P in K]: T[P] }
Means Omit<T, K> is actually: { [P in Exclude<keyof T, K>]: T[P] }

Now keyof T is using the keyof type operator to get a union type of all keys of T.
Using Exclude<.., K> we remove all keys K from this union.
Then we construct a mapped type with all remaining keys.

This is what I wrote first, which does not fully apply here:
Now the issue / misunderstanding you face is not with the intersection type (&), but just with the union type (|).
The keyof type operator will return all keys that are present in all of the types of the union. This behaviour is by design.
So in the case of { a: any; b: any; c: any } | { b: any; c: any; d: any } you will end up "b" | "c", because only these two keys are present in all of the types of the union.

The keyof type operator will return all keys of your type, which are "a" | "b" | "c" | "d" | "e".
Removing the key "e" (via Exclude<>) will now result in "a" | "b" | "c" | "d".
Using Pick<> you create a mapped type: { [P in keyof "a" | "b" | "c" | "d"]: T[P] }
Now the issue you face is how indexed access types behave with union types: You receive a union of the property types.
The property "a" of your first type is number, and of your second type is never | undefined.
The never type is basically an empty union type, so it gets removed from the union and you end up with number | undefined.
This means the property "a" of your final mapped type has the type number | undefined.

You expected Omit<> to operate on each case of your union type individually, but that's not how it works. It operates on the type you provide: Test - the union gets "squashed together".
But by (ab)using distributive conditional types you can get the behaviour you want, this way each type of your union is treated individually and you end up with another union type.

Here's an example implementation: https://stackoverflow.com/a/57103940


Hope that makes sense and is somewhat understandable.

@RowanDZ
Copy link
Author

RowanDZ commented Oct 5, 2021

Clear, thank you for the detailed answer ! :)

@RowanDZ RowanDZ closed this as completed Oct 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants