1

I've the following tagged union interface

interface Example {
  a: TypeA;
  b: TypeB;
}

as an output I would like to convert this tagged union into an union type such as:

type oneOf<T> = ...

Where

var example: oneOf(Example); // would be { a: TypeA } | { b : TypeB }

I have not been able to map the type keys into an object having only the key as parameter. Any idea ?

Flavien Volken
  • 14,820
  • 9
  • 78
  • 105

2 Answers2

2

You can do it with a combination of:

interface Example {
  a: string;
  b: number;
}

type SingleProperty<T, K extends keyof T> = K extends any ? {[Prop in K]: T[Prop]} : never
type UnionOfProperties<T> = { [K in keyof T]: SingleProperty<T, K> }[keyof T];
type ExamplePropertiesUnion = UnionOfProperties<Example>

This returns expected:

type ExamplePropertiesUnion = {
    a: string;
} | {
    b: number;
}

While the above is correct, TS will allow the following

var t: ExamplePropertiesUnion = {a: "", b: 42}

Which is NOT what we usually want:

Here below is the variant for a stricter type checking

type FixTsUnion<T, K extends keyof T> = {[Prop in keyof T]?: Prop extends K ? T[Prop]: never}
type oneOf<T> = { [K in keyof T]: Pick<T, K> & FixTsUnion<T, K>}[keyof T];
// ok
var z1: oneOf<Example> = { a: "" };
// ok
var z2: oneOf<Example> = { b: 5 };
// error
var z3: oneOf<Example> = { a: "", b: 34 };
// error
var z4: oneOf<Example> = { };

Try it here

See questions:

Flavien Volken
  • 14,820
  • 9
  • 78
  • 105
Lesiak
  • 12,048
  • 2
  • 17
  • 41
  • while this works I am now wondering why `var t: { a: string } | { b: number } = {a: "", b: 42}` does not raise an error. – Flavien Volken Jun 26 '20 at 10:04
  • I think TS does not support excess property checking for union types. I managed to google a few proposals around that: https://github.com/microsoft/TypeScript/issues/23535 https://github.com/Microsoft/TypeScript/issues/14094 You may try explicit never to work around that: `var t: { a: string, b: never } | { b: number, a: never } = {a: "", b: 42}` – Lesiak Jun 26 '20 at 10:18
  • Well good idea but it will fail for any simple affectation for instance: `var t: { a: string , b: never } | { a: never, b: string } = {a: "" }` – Flavien Volken Jun 26 '20 at 10:53
  • My bad. Sorry for the confusion. – Lesiak Jun 26 '20 at 11:00
  • Also we can rewrite UnionOfProperties to: `type oneOf = { [K in keyof T]: Pick }[keyof T];` your solution did look promising we should find a way to restrict a type to a maximum number or properties. – Flavien Volken Jun 26 '20 at 11:01
  • `var t: { a: string; b?: undefined } | { a?: string; b: undefined } = {a: "" }` seems to cut the mustard – Lesiak Jun 26 '20 at 11:21
  • Yes this is precisely what I was got, I was trying to include it into your answer using the "?" syntax. – Flavien Volken Jun 26 '20 at 11:24
  • `type oneOf = { [K in keyof T]: Pick & Partial> }[keyof T];` – Lesiak Jun 26 '20 at 11:33
  • this is closer: `type VariantWithKEnabled = K extends any ? {[Prop in keyof T]?: Prop extends K ? T[Prop]: never} : never ` But works on `t= {}`, while it should fail. Yours fail but works on `t={ a: "", b: 23}` – Flavien Volken Jun 26 '20 at 11:37
  • I checked and `const t: oneOf = { a: 'a', b: 'b' }` fails – Lesiak Jun 26 '20 at 11:57
  • 1
    because b should be number. I edited your answer, you deserve all the credits ;-) – Flavien Volken Jun 26 '20 at 11:59
0
type EntryUnion<T> = { [K in keyof T]: { [Q in K]: T[Q] } }[keyof T];

This is effectively the same as

type EntryUnion<T> = { [K in keyof T]: Pick<T, K> }[keyof T];

But whenever I use util types such as Pick, the type inference in both Intellij and VS Code stops at Pick and refuses to expand further. Although they do exactly the same job when it comes to enforcing type correctness at assignmen, when things go wrong it is less obvious what is incorrect with the type definition using Pick (unless you are very familiar with what Pick does)

Xinchao
  • 1,335
  • 9
  • 28
  • Hi, I take note of the shortcut, also your solution will work ensuring there is at least one key present but it won't ensure there are not duplicates (exclusive union on keys) [check here](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogdsATiAqnAlgezgHgCoB8UAvFAN5QDaA0lOnFANYQiYBmUeAugFzlUBFOg2q9OlAVygBfGZWasO3ANwAoVfWAREbAIYBjaAEk8EAM7Byq3XwuJ6AczUAjW0kdrp6-dgtQtFnzwSKgY2Dgm5sBEpGQ2AES68QA0UK7xzvHSylAA9LlQZgAWmACuADYAJlB66OWqQA) – Flavien Volken Apr 09 '21 at 07:17
  • 1
    Yes you are absolutely right. However, to me, this particular issue involves a trade off between strictness and clarity. Effectively the strictest type definition here should be (**inferred as**) `{ a: string, b: never } | { a: never, b: number }` . But it lacks in clarity especially when you have a lot more cases and when there is a mistake in assignment and code does not compile. The IDE is less helpful compared to that if the type is defined as `{ a: string } | { b: number }`. But you are still right, I cannot find an easy way to express exclusive keys in typescript – Xinchao Apr 09 '21 at 07:33