Describe The Problem To Be Solved
Currently MatchTag uses overloads to determine user's intent.
While it is straightforward when reading the declaration, when there are missing cases, it generates an error message that is hard to understand:
type MyUnion =
| {
kind: "foo";
foo: "foo-value";
}
| {
kind: "bar";
bar: "bar-value";
};
<MatchTag
on={value()}
// No overload matches this call.
// The last overload gave the following error.
// Type 'MyUnion' is not assignable to type '{ type: PropertyKey; } | null | undefined'.
// Property 'type' is missing in type '{ kind: "foo"; foo: "foo-value"; }' but required in type '{ type: PropertyKey; }'.
tag="kind"
case={{
}}
/>;
As you can see, the error message only shows type is missing from your on value, which is the result of evaluating the last overload. (to be honest, because of this I thought the author did not consider tag's value in the type declaration at first glance.)
This is quite confusing, isn't it?
Suggest A Solution
I suggest unifying the overloads using conditional types.
My sketch:
Playground
Code block (long)
import type { Accessor, JSX } from 'solid-js'
type Cases<IsPartial extends boolean, T> =
boolean extends IsPartial
? { 'no `boolean` allowed, please use either `true` or `false`': never }
: true extends IsPartial
? Partial<T>
: Required<T>
type NotProvidedOrTyped<K extends string, V, Default> =
| (V extends Default ? Partial<Record<K, never>> : never)
| Required<Record<K, V>>
type Tag = string | number
type TagKeyDefault = "type"
type PartialDefault = false
declare function MatchTag<
T extends { [k in TagKey]: Tag },
TagKey extends string = TagKeyDefault,
IsPartial extends boolean = PartialDefault,
>(
props:
& {
on: T | null | undefined;
case: Cases<IsPartial, { [Tag in T[TagKey]]: (v: Accessor<Extract<T, Record<TagKey, Tag>>>) => JSX.Element }>;
fallback?: JSX.Element;
}
& NotProvidedOrTyped<"tag", TagKey, TagKeyDefault>
& NotProvidedOrTyped<"partial", IsPartial, PartialDefault>
): JSX.Element;
type MyUnion =
| {
kind: "foo";
foo: "foo-value";
}
| {
kind: "bar";
bar: "bar-value";
};
declare const value: () => MyUnion
// normal case
<MatchTag
on={value()}
tag="kind"
case={{
foo: props => <>{props().foo}</>,
bar: props => <>{props().bar}</>,
}}
/>;
// edge cases
<MatchTag
on={value()}
tag="kind"
case={{}}
// much more clear error message:
// Type '{}' is missing the following properties from
// type 'Required<{
// foo: (v: Accessor<{ kind: "foo"; foo: "foo-value"; }>) => Element;
// bar: (v: Accessor<{ kind: "bar"; bar: "bar-value"; }>) => Element;
// }>': foo, bar
/>;
<MatchTag<MyUnion, "kind">
// properly handles "explicit generic argument without providing optional arguments" hazard (see https://github.com/microsoft/TypeScript/issues/58977)
on={value()}
// `tag` prop is not actually provided
case={{
foo: () => <></>,
bar: () => <></>,
}}
/>;
<MatchTag<MyUnion, "kind", true>
// handles `partial` too!
on={value()}
tag="kind"
case={{}}
// `partial` prop is not actually provided
/>;
<MatchTag<MyUnion, "kind", boolean>
on={value()}
tag="kind"
case={{}} // bans ambiguous `partial: boolean`
/>;
type MyUnionWithTypeTag =
| {
type: "foo";
foo: "foo-value";
}
| {
type: "bar";
bar: "bar-value";
};
declare const valueTypeTag: () => MyUnionWithTypeTag
// normal case
<MatchTag
on={valueTypeTag()}
case={{
foo: props => <>{props().foo}</>,
bar: props => <>{props().bar}</>,
}}
/>;
// edge cases
<MatchTag
on={valueTypeTag()}
case={{}}
/>;
I tried to write the code easy to read, but I understand it still is hard to read.
Feedbacks appreciated!
Describe The Problem To Be Solved
Currently
MatchTaguses overloads to determine user's intent.While it is straightforward when reading the declaration, when there are missing cases, it generates an error message that is hard to understand:
As you can see, the error message only shows
typeis missing from youronvalue, which is the result of evaluating the last overload. (to be honest, because of this I thought the author did not considertag's value in the type declaration at first glance.)This is quite confusing, isn't it?
Suggest A Solution
I suggest unifying the overloads using conditional types.
My sketch:
Playground
Code block (long)
I tried to write the code easy to read, but I understand it still is hard to read.
Feedbacks appreciated!