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
withCustomInput
key is true,Prop1
interface will be the type forto
key. - if
withCustomInput
key is false,Prop2
interface will be the type forto
key.
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.