Volox
Volox

Reputation: 1108

Typescript union types not excluded

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

Answers (3)

raina77ow
raina77ow

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

Amir Saleem
Amir Saleem

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

phry
phry

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

Related Questions