Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UX issues when inspecting types (truncation, type aliases not used) #60

Open
OliverJAsh opened this issue Jul 3, 2019 · 3 comments
Open

Comments

@OliverJAsh
Copy link
Collaborator

OliverJAsh commented Jul 3, 2019

We're using Unionize pretty heavily at Unsplash (thank you!). I would like to share some UX feedback relating to how the union types appear when inspected. To do so, I will use an example.

Vanilla tagged union

First let's see how type inspection works for a sans-Unionize union:

type Foo = { foo: number };
type Bar = { bar: number };
type Baz = { baz: number };

type MyNestedUnion =
  | {
      tag: "NestedFoo";
      value: Foo;
    }
  | {
      tag: "NestedBar";
      value: Bar;
    }
  | {
      tag: "NestedBaz";
      value: Baz;
    };

// Hover this
type MyUnion =
  | {
      tag: "Foo";
      value: Foo;
    }
  | {
      tag: "Bar";
      value: Bar;
    }
  | {
      tag: "Baz";
      value: Baz;
    }
  | {
      tag: "Union";
      value: MyNestedUnion;
    };

// Hover this
declare const fn: (f: MyUnion) => void;

Hover over type MyUnion and you'll see:

type MyUnion =
  | {
      tag: "Foo";
      value: Foo;
    }
  | {
      tag: "Bar";
      value: Bar;
    }
  | {
      tag: "Baz";
      value: Baz;
    }
  | {
      tag: "Union";
      value: MyNestedUnion;
    };

Great! Hover over const fn and you'll see:

const fn: (f: MyUnion) => void;

Great! No problems here.

Unionize union

Now if we define the equivalent tagged union but using Unionize:

import { ofType, unionize, UnionOf } from "unionize";

type Foo = { foo: number };
type Bar = { bar: number };
type Baz = { baz: number };

const MyNestedUnion = unionize(
  {
    NestedFoo: ofType<Foo>(),
    NestedBar: ofType<Bar>(),
    NestedBaz: ofType<Baz>(),
  },
  { value: "value" },
);
type MyNestedUnion = UnionOf<typeof MyNestedUnion>;

const MyUnion = unionize(
  {
    Foo: ofType<Foo>(),
    Bar: ofType<Bar>(),
    Baz: ofType<Baz>(),
    Union: ofType<MyNestedUnion>(),
  },
  { value: "value" },
);
// Hover this
type MyUnion = UnionOf<typeof MyUnion>;

// Hover this
declare const fn: (f: MyUnion) => void;

Now hover over type MyUnion and you'll see:

type MyUnion = ({
    tag: "Foo";
} & {
    value: Foo;
}) | ({
    tag: "Bar";
} & {
    value: Bar;
}) | ({
    tag: "Baz";
} & {
    value: Baz;
}) | ({
    tag: "Union";
} & {
    value: ({
        tag: "NestedFoo";
    } & {
        value: Foo;
    }) | ({
        tag: "NestedBar";
    } & {
        ...;
    }) | ({
        ...;
    } & {
        ...;
    });
})

Observations:

  1. {
        tag: "Foo";
    } & {
        value: Foo;
    }
    … could be simplified to
    {
      tag: "Foo";
      value: Foo;
    }
    Is this something TS should do automatically for us? In the interim, is this something we can workaround, e.g. using Compact (which "cleans" types with lots of arithmetic)?
  2. In the sans-Unionize example, the MyNestedUnion type alias was shown nested inside of here. However in this Unionize example, the type alias MyNestedUnion is thrown away and the whole type structure is shown instead. Why? Is this a design limitation or bug in TS? Is it something we can workaround?
  3. Some types are truncated. This makes these types really difficult to work with. Often times, inspecting a type is not enough to understand how to work with it—instead you have to look deep into the definition. If we can't fix 2 (doing so would negate this), can we prevent truncation somehow?

Hover over const fn and you'll see:

const fn: (f: ({
    tag: "Foo";
} & {
    value: Foo;
}) | ({
    tag: "Bar";
} & {
    value: Bar;
}) | ({
    tag: "Baz";
} & {
    value: Baz;
}) | ({
    tag: "Union";
} & {
    value: ({
        tag: "NestedFoo";
    } & {
        value: Foo;
    }) | ({
        tag: "NestedBar";
    } & {
        ...;
    }) | ({
        ...;
    } & {
        ...;
    });
})) => void

In the sans-Unionize example, the MyUnion type alias was shown here. However in this Unionize example, the type alias MyUnion is thrown away and the whole type structure is shown instead. Why?

I appreciate there might not be much we can do from Unionize's side to help address these UX issues. However I imagine there's at least some discussions inside of TS we could chime into, so our voice is heard. 🤞

@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented Jul 7, 2019

Observation 3:

import { ofType, unionize, UnionOf } from 'unionize';

const MyUnion = unionize({
    Foo: ofType<{ foo: number; bar: number; baz: number }>(),
});

MyUnion.Foo();

image

Pick<{ foo: number; bar: number; baz: number; }, "foo" | "bar" | "baz"> could be simplified to the original type, { foo: number; bar: number; baz: number }.

@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented Jul 8, 2019

Regarding observation 1, Compact only seems to help when

type Foo = { foo: number };
type Bar = { bar: number };

const MyUnion = unionize(
  {
    Foo: ofType<Foo>(),
    // Bar: ofType<Bar>(),
  }
);
// Hover this
type MyUnion = UnionOf<typeof MyUnion>;

Hovering over MyUnion produces:

{
    tag: "Foo";
    foo: number;
}

… but enable Bar and it produces:

Compact<{
    tag: "Foo";
} & Foo> | Compact<{
    tag: "Bar";
} & Bar>

😢

Regarding observation 2, TypeScript issue for this: microsoft/TypeScript#32287

Regarding observation 3

  • This appears to be due to UnTagged.
  • Wrapping UnTagged in Compact does not seem to help.
  • I think we need to remove UnTagged anyway to fix Nested unions are "untagged" #61.

@OliverJAsh
Copy link
Collaborator Author

Regarding these issues:

2. In the sans-Unionize example, the MyNestedUnion type alias was shown nested inside of here. However in this Unionize example, the type alias MyNestedUnion is thrown away and the whole type structure is shown instead. Why? Is this a design limitation or bug in TS? Is it something we can workaround?

and

Hover over const fn and you'll see:

const fn: (f: ({
    tag: "Foo";
} & {
    value: Foo;
}) | ({
    tag: "Bar";
} & {
    value: Bar;
}) | ({
    tag: "Baz";
} & {
    value: Baz;
}) | ({
    tag: "Union";
} & {
    value: ({
        tag: "NestedFoo";
    } & {
        value: Foo;
    }) | ({
        tag: "NestedBar";
    } & {
        ...;
    }) | ({
        ...;
    } & {
        ...;
    });
})) => void

In the sans-Unionize example, the MyUnion type alias was shown here. However in this Unionize example, the type alias MyUnion is thrown away and the whole type structure is shown instead. Why?

It seems the problem happens when we use UnionOf. If we switch to something else, both of these issues disappear:

-type MyUnion = UnionOf<typeof MyUnion>;
+type MyUnionTaggedRecord = TaggedRecordOf<typeof MyUnion>;
+type MyUnion = MyUnionTaggedRecord[keyof MyUnionTaggedRecord];

Complete example:

import { ofType, unionize, TaggedRecordOf } from "unionize";

type Foo = { foo: number };
type Bar = { bar: number };
type Baz = { baz: number };

const MyNestedUnion = unionize(
    {
        NestedFoo: ofType<Foo>(),
        NestedBar: ofType<Bar>(),
        NestedBaz: ofType<Baz>(),
    },
    { value: 'value' },
);
type MyNestedUnionTaggedRecord = TaggedRecordOf<typeof MyNestedUnion>;
type MyNestedUnion = MyNestedUnionTaggedRecord[keyof MyNestedUnionTaggedRecord];

const MyUnion = unionize(
    {
        Foo: ofType<Foo>(),
        Bar: ofType<Bar>(),
        Baz: ofType<Baz>(),
        Union: ofType<MyNestedUnion>(),
    },
    { value: 'value' },
);
type MyUnionTaggedRecord = TaggedRecordOf<typeof MyUnion>;
// Hover this
type MyUnion = MyUnionTaggedRecord[keyof MyUnionTaggedRecord];

// Hover this
declare const fn: (f: MyUnion) => void;

Perhaps we should discourage UnionOf in favour of this. WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant