Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions public-types/reflect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props, Bind extends BindFromProps<Props>> = Show<
Props extends any
? Omit<Props, keyof Bind> & {
[K in Extract<keyof Bind, keyof Props>]?: Props[K];
}
: never
>;

/**
* Operator to conditionally render a component based on the reactive `source` store value.
*
Expand All @@ -206,8 +222,9 @@ export function list<
* ```
*/
export function variant<
Props,
CaseType extends string,
Cases extends Record<CaseType, ComponentType<any>>,
Props extends ComponentProps<Cases[CaseType]>,
// 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<string, never> or any option other than `{}` doesn't work
// eslint-disable-next-line @typescript-eslint/ban-types
Expand All @@ -216,7 +233,7 @@ export function variant<
config:
| {
source: Store<CaseType>;
cases: Partial<Record<CaseType, ComponentType<Props>>>;
cases: Partial<Cases>;
default?: ComponentType<Props>;
bind?: Bind;
hooks?: Hooks<Props>;
Expand All @@ -236,7 +253,7 @@ export function variant<
*/
useUnitConfig?: UseUnitConfig;
},
): FC<FinalProps<Props, Bind>>;
): FC<FinalPropsVariant<Props, Bind>>;

// fromTag types
/**
Expand Down
116 changes: 106 additions & 10 deletions type-tests/types-variant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ import { expectType } from 'tsd';
},
});

expectType<React.FC>(VariableInput);
<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;
Expand All @@ -56,12 +56,38 @@ import { expectType } from 'tsd';
},
cases: {
input: Input,
// @ts-expect-error
datetime: DateTime,
},
});

expectType<React.FC>(VariableInput);
<VariableInput />;
<VariableInput value="test" />;
<VariableInput
value="test"
onChange={(event: { target: { value: string } }) => {
event.target.value;
}}
/>;
<VariableInput
value="test"
onChange={() => {
// ok
}}
/>;
<VariableInput
value="test"
onChange={(event: string) => {
event;
}}
/>;
<VariableInput
// @ts-expect-error
value={42}
// @ts-expect-error
onChange={(event: number) => {
event;
}}
/>;
}

// variant allows not to set every possble case
Expand Down Expand Up @@ -89,7 +115,10 @@ import { expectType } from 'tsd';
default: NotFoundPage,
});

expectType<React.FC>(CurrentPage);
<CurrentPage />;
<CurrentPage context={{ route: 'home' }} />;
// @ts-expect-error
<CurrentPage context="kek" />;
}

// variant warns about wrong cases
Expand Down Expand Up @@ -117,7 +146,10 @@ import { expectType } from 'tsd';
default: NotFoundPage,
});

expectType<React.FC>(CurrentPage);
<CurrentPage />;
<CurrentPage context={{ route: 'home' }} />;
// @ts-expect-error
<CurrentPage context="kek" />;
}

// overload for boolean source
Expand All @@ -140,14 +172,21 @@ import { expectType } from 'tsd';
else: FallbackPage,
bind: { context: $ctx },
});
expectType<React.FC>(CurrentPageThenElse);

<CurrentPageThenElse />;
<CurrentPageThenElse context={{ route: 'home' }} />;
// @ts-expect-error
<CurrentPageThenElse context="kek" />;

const CurrentPageOnlyThen = variant({
if: $enabled,
then: HomePage,
bind: { context: $ctx },
});
expectType<React.FC>(CurrentPageOnlyThen);
<CurrentPageOnlyThen />;
<CurrentPageOnlyThen context={{ route: 'home' }} />;
// @ts-expect-error
<CurrentPageOnlyThen context="kek" />;
}

// supports nesting
Expand All @@ -169,6 +208,8 @@ import { expectType } from 'tsd';
}),
},
});

<NestedVariant />;
}

// allows variants of compatible types
Expand All @@ -188,6 +229,10 @@ import { expectType } from 'tsd';
}),
else: Loader,
});

<View test="test" />;
// @ts-expect-error
<View test={42} />;
}

// Issue #81 reproduce 1
Expand Down Expand Up @@ -264,7 +309,6 @@ import { expectType } from 'tsd';
},
cases: {
button: Button<'button'>,
// @ts-expect-error
a: Button<'a'>,
},
});
Expand All @@ -277,15 +321,67 @@ import { expectType } from 'tsd';
},
cases: {
button: Button<'button'>,
// @ts-expect-error
a: Button<'a'>,
},
});

<ReflectedVariantBad />;
<ReflectedVariantBad size="xl" />;
// @ts-expect-error
<ReflectedVariantBad size={52} />;

const IfElseVariant = variant({
if: createStore(true),
then: Button<'button'>,
// @ts-expect-error
else: Button<'a'>,
});

<IfElseVariant />;
<IfElseVariant size="xl" />;
// @ts-expect-error
<IfElseVariant size={52} />;
}

// 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<string>('');
const changed = createEvent<string>();

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 <ReflectedInput />;
};

<ReflectedInput kek="kek" />;

const AppFixed: React.FC = () => {
return <ReflectedInput color="red" />;
};
expectType<React.FC>(App);
expectType<React.FC>(AppFixed);
}