Issue
I am trying to create an interface which involves conditional types but I have an issue related to optional parameter.
I receive
Type 'false' is not assignable to type 'true' error for my boolean property.
react-select library is inspired me to build this behaviour. They have used it to handle isMulti property to decide value is either MultiValue or SingleValue
export declare type OnChangeValue<Option, IsMulti extends boolean> = IsMulti extends true ? MultiValue<Option> : SingleValue<Option>;
reference link: https://github.com/JedWatson/react-select/blob/master/packages/react-select/src/Select.tsx
interface Prop1 {
iconPosition: string;
}
interface Prop2 {}
export interface IDateRangeFilter<
V extends object,
WithCustomInputT extends boolean = true
> {
to: Partial<WithCustomInputT extends false ? Prop2 : Prop1>;
fromName: keyof V;
withCustomInput: WithCustomInputT;
}
export interface IFilterOptions<KeyType extends keyof any, V extends object> {
field?: KeyType;
date?: IDateRangeFilter<V>;
}
export type IFilterOptionsSingleType<T extends object> = IFilterOptions<
keyof T,
T
>;
interface Ifilter {
ActivityStartDate: string;
}
// final declaration
export const ReferralsFilters: IFilterOptionsSingleType<Ifilter>[] = [
{
date: {
to: {
iconPosition: "end",
},
fromName: "ActivityStartDate",
withCustomInput: false, // Error: Type 'false' is not assignable to type 'true'
},
},
];
The aim here is that
- if
withCustomInputkey is true,Prop1interface will be the type fortokey. - if
withCustomInputkey is false,Prop2interface will be the type fortokey.
Solution
The main problem with the code in the question as asked is that you give IDateRangeFilter<V> a default generic type argument of true and never pass in any explicit type argument for the date field of IFilterOptions, so the default of true is always used. You can fix that by explicitly saying that date can be either IDateRangeFilter<V, true> or IDateRangeFilter<V, false>. That is, use a union:
interface IFilterOptions<KeyType extends keyof any, V extends object> {
field?: KeyType;
date?: IDateRangeFilter<V, true> | IDateRangeFilter<V, false>;
}
That's the answer to the question as asked. But since unions are not exclusive in TypeScript, and since object types are not sealed (a request for sealed types is at microsoft/TypeScript#12936), you'll need to do more work if you want to prohibit, say, an iconPosition property for Prop2. See Why does A | B allow a combination of both, and how can I prevent it? for a relevant question and its answer.
We can define an AndNot<T, U> utility type so that if T and U are object types with some non-overlapping property keys, the type AndNot<T, U> will allow a T but prohibit a U by requiring that any keys of U not present in T be missing (well, optional keys whose value is of type never):
type AndNot<T, U> = T & { [K in Exclude<keyof U, keyof T>]?: never }
And now your definition of IDateRangeFilter can use AndNot<Prop2, Prop1> and AndNot<Prop1, Prop2> to distinguish the types from each other:
interface IDateRangeFilter<
V extends object,
WithCustomInputT extends boolean
> {
to: Partial<WithCustomInputT extends false ?
AndNot<Prop2, Prop1> : AndNot<Prop1, Prop2>>;
fromName: keyof V;
withCustomInput: WithCustomInputT;
}
Let's test it:
const ReferralsFilters: IFilterOptionsSingleType<Ifilter>[] = [
{
date: {
to: {
iconPosition: "end",
},
fromName: "ActivityStartDate",
withCustomInput: true,
},
},
{
date: {
to: {},
fromName: "ActivityStartDate",
withCustomInput: false,
},
},
{
date: {
to: {
iconPosition: "end", // error!
},
fromName: "ActivityStartDate",
withCustomInput: false,
},
},
];
Looks good; iconPosition is prohibited when withCustomInput is false, and allowed when it is true, as desired.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.