39

Is it possible to map a union type to another union type in TypeScript?

What I'd Like to be able to do

e.g. Given a union type A:

type A = 'one' | 'two' | 'three';

I'd like to be able to map it to union type B:

type B = { type: 'one' } | { type: 'two'} | { type: 'three' };

What I have tried

type B = { type: A };

But this results in:

type B = { type: 'one' | 'two' | 'three' };

which is not quite what I want.

bingles
  • 11,582
  • 10
  • 82
  • 93

2 Answers2

63

You can use conditional type for distributing over the members of the union type (conditional type always takes only one branch and is used only for its distributive property, I learned this method from this answer)

type A = 'one' | 'two' | 'three';

type Distribute<U> = U extends any ? {type: U} : never;

type B = Distribute<A>;

/*
type B = {
    type: "one";
} | {
    type: "two";
} | {
    type: "three";
}
*/
artem
  • 46,476
  • 8
  • 74
  • 78
  • This worked. Out of curiosity how does the `U extends {}` resolve as truthy to provide { type: U } ? – bingles Aug 05 '18 at 15:37
  • 1
    `U extends {}` is true for object types, "value" types like `string`, `number` and `boolean` and literals of those value types. It's false if `U` is `undefined` and `void`, so the statement "always takes only one branch" is not true, `U extends {}` excludes `void` and `undefined` from the union. I updated the answer to change it to `U extends any` because I don't know if it's actually desirable to exclude types like `void` and `undefined`. – artem Aug 05 '18 at 15:49
  • Thanks for the clarification. I was suprised that string literals extend {} – bingles Aug 06 '18 at 11:10
  • `{}` describes a type with no properties, therefore every value other than `undefined` and `null` match that description (they are a superset of "an object with no properties"). – nickf Sep 21 '21 at 06:53
  • 1
    How do the inverse? From type B `{ type: 'one' } | { type: 'two'} | { type: 'three' }` to type A `'one' | 'two' | 'three'`? – kaptux Nov 17 '21 at 15:43
  • 2
    Ok, this does the job: `type PickFieldTypes = U extends any ? U[K] : never;`. Great!! – kaptux Nov 17 '21 at 15:56
  • Wow this is terrible language design... the lhs and rhs of a type alias should be interchangable! Meaning `type B = A extends any ? {type: A} : never` should have the same effect...but it doesn't do the same thing as `type B = Distribute`. Also there ought to be a way to distribute `'one' | 'two' | 'three'` inline without any type aliases at all. – Andy May 03 '23 at 03:04
0

I have found another solution that uses a utility type, but I'm not necessarily recommending it: I'm still learning typescript:

type A = 'one' | 'two' | 'three';
// type B = { type: 'one' } | { type: 'two'} | { type: 'three' };

type B = { type: Extract<A, any> }; // extract all from union
//   ^?
const one: B = { type: 'one' };
const two: B = { type: 'two' };
const three: B = { type: 'three' };
const four: B = { type: 'four' }; // errors

FWIW, the definition of Extract is type Extract<T, U> = T extends U ? T : never;

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356