Reputation: 1108
I have an issue with union types and Exclude
(playground link):
type Foo = {
data: string;
otherData: number;
};
type Bar = Omit<Foo,'otherData'>
// type Bar = { data: string; }; // Even this is not working
type FooBar = Foo | Bar;
type ShouldBeFoo = Exclude<FooBar, Bar>; // Not working
// ShouldBeFoo = never
type ShouldBeBar = Exclude<FooBar, Foo>; // Somehow working
// ShouldBeBar = { data: string }
Am I missing something related to union types and/or Exclude
?
I also tried with TS 4.4 and 4.2 with the same result.
Note:
We discovered the issue using type guards, you can see it in the playground here.
Upvotes: 4
Views: 456
Reputation: 106443
It seems the real issue is misunderstanding how extends
check works in TS. Quoting the docs:
SomeType extends OtherType ? TrueType : FalseType;
When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).
In this case, SomeType doesn't have to be an explicit extension of OtherType (in class SomeType extends OtherType
sense): it's enough for SomeType
to be assignable to OtherType
, to have all the properties (of the same type) as OtherType
.
As a side effect, when type Y
is a result of Omit<X, ...>
, X extends Y
check always passes (!), as Omit
only reduces the number of properties, but doesn't change their types and/or create new ones:
type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }
Now, as Exclude
generic actually looks like this:
type Exclude<T, U> = T extends U ? never : T
... your union type (of Foo | Bar
) passes extends Bar
check just fine, as both parts of union pass it. However, Bar extends Foo
check fails, that's why your expression works fine for shouldBeBar
case.
Upvotes: 1
Reputation: 3140
Utility types like Pick
, Omit
work well with interface or types with properties since the second arg looks of keyof K
.
As yours is a union type, the TS compiler is unable to infer the correct behavior. You can try this
type Foo = {
data: string;
otherData: number;
};
type Bar = Omit<Foo, 'otherData'>
type FooBar = Foo | Bar;
type Reduce<T, K> = T extends K ? K : never;
type ShouldBeFoo = Reduce<FooBar, Foo>;
type ShouldBeBar = Reduce<FooBar, Bar>;
Upvotes: 1
Reputation: 44166
Exclude excludes types that are matching, not equal.
You are excluding Bar
from FooBar
. Bar
matches Bar
and Bar
matches Foo
. So you exclude both.
You could use something such as
type ExcludeExact<T, U> = T extends U ? U extends T ? T : never : never
Upvotes: 1