33

I have the following Union type;

type MyUnionType = 'foo' | 'bar' | 'baz'

I would like to create a new Union MySubUnion as a subset;

type MySubUnion = 'foo' | 'bar'

I would like MySubUnion to be constrained to the values of its parent MyUnionType

type MySubUnion = 'foo' | 'bas' // => Error Type String 'b...
Barris
  • 969
  • 13
  • 29

4 Answers4

58

Restricting a union to a subset of consituents is subtyping. In TypeScript, A extends B is a way of saying that A is a subtype of B. (This seems backwards to some people at some times; by removing elements from a union, you are making the type more specific, which is a subtype. The word "extends" might seem out of place, but that's what it is).

Unfortunately, you can't use extends to narrow type aliases the way you can with interfaces. What you'd like to do is use following invalid syntax:

// this is not valid TypeScript, do not use this:
type MySubUnion extends MyUnionType = 'foo' | 'bar'; // should work
type MySubUnion extends MyUnionType = 'foo' | 'bas'; // should fail

But you can't do that. As a workaround, you can make a new type function called Extends<T, U> which evaluates to U but only compile if U extends T, like this:

type Extends<T, U extends T> = U;

Then you can rewrite the invalid code to the following valid code:

type MySubUnion = Extends<MyUnionType, 'foo' | 'bar'>; // okay, compiles

and this:

type MySubUnion = Extends<MyUnionType, 'foo' | 'bas'>; // error:
// Type '"bas"' is not assignable to type 'MyUnionType'.

Does that help? Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Extremely helpful thank you! I was not aware of type functions – Barris Dec 06 '18 at 08:02
  • Perfect, that was exactly what i was searching for! Thanks a LOT! – Astra-Ritter Jul 29 '21 at 10:17
  • Why isn't the `Extends` function already included in TypeScript? Seems redundant to add it manually into every project when you just need to make a subset of a union of strings accepted somewhere. – a_rts Apr 02 '23 at 17:20
  • They don’t add utility types to the language unless they need the types for emitting declaration files. If you have more detailed questions then you might want to ask them in a new post instead of hoping that someone will respond to comments on answers to years-old questions. Good luck! – jcalz Apr 02 '23 at 17:39
10

Use the Exclude<Type, Union> utility type.

type MyUnionType = 'foo' | 'bar' | 'baz'

type SubType = Exclude<MyUnionType, 'baz'>

// SubType is now 'foo' | 'bar'
Thomas Deutsch
  • 2,344
  • 2
  • 27
  • 36
4

You could always flip the declaration order, though it's a bit odd with the particular names here.

type MySubUnion = 'foo' | 'bar';
type MyUnionType = MySubUnion | 'baz';

It's more in the lines of composing union types.

What Would Be Cool
  • 6,204
  • 5
  • 45
  • 42
2

As mentioned in this post:

Possible to extend types in Typescript?

You can just write like that:

type MySubUnion = MyUnionType & ('foo' | 'bar');
  • 1
    This works, but unfortunately does not warn/fail if you accidentally create a never type. E.g. `type MySubUnion = MyUnionType & 'someOtherLiteral'` will make `MySubUnion` be `never` without a warning. – 203 Mar 03 '23 at 07:43