0

I have a type that looks like this.

type Focus =
    | { sidebar: true }
    | { tabbar: true }
    | { menu: string }

The whole point of this type structure is the convenience of being able to do things like if (focus.sidebar) or if (focus.menu == "open") so I don't want to rewrite my code as if ("sidebar" in focus) and if ("menu" in focus && focus.menu === "open").

This type would technically work for me:

type Focus =
    | { sidebar: true; tabbar?: undefined; menu?: undefined }
    | { tabbar: true; sidebar?: undefined; menu?: undefined }
    | { menu: string; sidebar?: undefined; tabbar?: undefined }

But this is really inconvenient to write.

I've made it this far:

type PartialEmpty<T> = { [A in keyof T]?: undefined }

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never

type Focus2 = PartialEmpty<UnionToIntersection<Focus>>
// {
//     sidebar?: undefined;
//     tabbar?: undefined;
//     menu?: undefined;
// }

But I can't figure out how to intersect and distribute that type over the union... This is pretty close:

type Distribute<T> = T extends any ? Omit<T, keyof Focus2> & Focus2 : never

type Focus3 = Distribute<Focus>

But it doesn't quite work...

const focus: Focus3 = {} as any
if (focus.menu) {
    focus.m
}

Playground Link

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Chet
  • 18,421
  • 15
  • 69
  • 113
  • 2
    Maybe try adding a field to discriminate between the types: `type Focus = { type: "sidebar", sidebar: true } | { type: "tabbar", tabbar: true } | { type: "menu", menu: string };` – thesilican Dec 26 '21 at 08:34
  • Check out [`type-fest`](https://github.com/sindresorhus/type-fest) utility typing library. It has this [`MergeExclusive`](https://github.com/sindresorhus/type-fest/blob/main/source/merge-exclusive.d.ts) type that does what you need, among other utility types. If you don't prefer installing it as devDependency, you can still reference how they do it. – yqlim Dec 26 '21 at 08:41

1 Answers1

1

T in Distribute<T> is one of:

| { sidebar: true }
| { tabbar: true }
| { menu: string }

So you need to omit keyof T from Focus2 and intersect with T:

type Distribute<T> = T extends any ? Omit<Focus2, keyof T> & T : never

Playground

Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
  • That works! Any ideas how to make output type a bit cleaner? It's pretty hard to interpret: `Omit, "sidebar"> & {sidebar: true;}` – Chet Dec 26 '21 at 22:03
  • 1
    You could use something like `type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never;` from https://stackoverflow.com/a/57683652/1113002 . And `type Focus3 = Expand>` – Aleksey L. Dec 27 '21 at 06:40