diff --git a/public-types/reflect.d.ts b/public-types/reflect.d.ts index ec9bb9e..6be4863 100644 --- a/public-types/reflect.d.ts +++ b/public-types/reflect.d.ts @@ -184,6 +184,22 @@ export function list< // variant types +/** + * Computes final props type based on Props of the view component and Bind object for variant operator specifically + * + * Difference is important since in variant case Props is a union + * + * Props that are "taken" by Bind object are made **optional** in the final type, + * so it is possible to overwrite them in the component usage anyway + */ +type FinalPropsVariant> = Show< + Props extends any + ? Omit & { + [K in Extract]?: Props[K]; + } + : never +>; + /** * Operator to conditionally render a component based on the reactive `source` store value. * @@ -206,8 +222,9 @@ export function list< * ``` */ export function variant< - Props, CaseType extends string, + Cases extends Record>, + Props extends ComponentProps, // It is ok here - it fixed bunch of type inference issues, when `bind` is not provided // but it is not clear why it works this way - Record or any option other than `{}` doesn't work // eslint-disable-next-line @typescript-eslint/ban-types @@ -216,7 +233,7 @@ export function variant< config: | { source: Store; - cases: Partial>>; + cases: Partial; default?: ComponentType; bind?: Bind; hooks?: Hooks; @@ -236,7 +253,7 @@ export function variant< */ useUnitConfig?: UseUnitConfig; }, -): FC>; +): FC>; // fromTag types /** diff --git a/type-tests/types-variant.tsx b/type-tests/types-variant.tsx index ca9bdc4..8a22ea2 100644 --- a/type-tests/types-variant.tsx +++ b/type-tests/types-variant.tsx @@ -31,10 +31,10 @@ import { expectType } from 'tsd'; }, }); - expectType(VariableInput); + ; } -// variant catches incompatible props between cases +// variant allows to pass incompatible props between cases - resulting component will have union of all props from all cases { const Input: React.FC<{ value: string; @@ -56,12 +56,38 @@ import { expectType } from 'tsd'; }, cases: { input: Input, - // @ts-expect-error datetime: DateTime, }, }); - expectType(VariableInput); + ; + ; + { + event.target.value; + }} + />; + { + // ok + }} + />; + { + event; + }} + />; + { + event; + }} + />; } // variant allows not to set every possble case @@ -89,7 +115,10 @@ import { expectType } from 'tsd'; default: NotFoundPage, }); - expectType(CurrentPage); + ; + ; + // @ts-expect-error + ; } // variant warns about wrong cases @@ -117,7 +146,10 @@ import { expectType } from 'tsd'; default: NotFoundPage, }); - expectType(CurrentPage); + ; + ; + // @ts-expect-error + ; } // overload for boolean source @@ -140,14 +172,21 @@ import { expectType } from 'tsd'; else: FallbackPage, bind: { context: $ctx }, }); - expectType(CurrentPageThenElse); + + ; + ; + // @ts-expect-error + ; const CurrentPageOnlyThen = variant({ if: $enabled, then: HomePage, bind: { context: $ctx }, }); - expectType(CurrentPageOnlyThen); + ; + ; + // @ts-expect-error + ; } // supports nesting @@ -169,6 +208,8 @@ import { expectType } from 'tsd'; }), }, }); + + ; } // allows variants of compatible types @@ -188,6 +229,10 @@ import { expectType } from 'tsd'; }), else: Loader, }); + + ; + // @ts-expect-error + ; } // Issue #81 reproduce 1 @@ -264,7 +309,6 @@ import { expectType } from 'tsd'; }, cases: { button: Button<'button'>, - // @ts-expect-error a: Button<'a'>, }, }); @@ -277,15 +321,67 @@ import { expectType } from 'tsd'; }, cases: { button: Button<'button'>, - // @ts-expect-error a: Button<'a'>, }, }); + ; + ; + // @ts-expect-error + ; + const IfElseVariant = variant({ if: createStore(true), then: Button<'button'>, // @ts-expect-error else: Button<'a'>, }); + + ; + ; + // @ts-expect-error + ; +} + +// variant should allow not-to pass required props - as they can be added later in react +{ + const Input: React.FC<{ + value: string; + onChange: (newValue: string) => void; + color: 'red'; + }> = () => null; + const $variants = createStore<'input' | 'fallback'>('input'); + const Fallback: React.FC<{ kek?: string }> = () => null; + const $value = createStore(''); + const changed = createEvent(); + + const InputBase = reflect({ + view: Input, + bind: { + value: $value, + onChange: changed, + }, + }); + + const ReflectedInput = variant({ + source: $variants, + cases: { + input: InputBase, + fallback: Fallback, + }, + }); + + const App: React.FC = () => { + // missing prop must still be required in react + // but in this case it is not required, as props are conditional union + return ; + }; + + ; + + const AppFixed: React.FC = () => { + return ; + }; + expectType(App); + expectType(AppFixed); }