Reputation: 1585
I want to declare an interface for a Select component that can be a multiple or single items select.
interface MySelect<T extends boolean> {
multi: T, // Is this a multiple items select
onChange: (item: T extends true ? string[]: string) => void // the onChange signature differs according to T
}
It works but I have to explicitly sets the generic type T:
const mySelect: MySelect<true> = { // Here
multi: true, // And here
onChange: (items) => {}
}
I would like to know if it's possible to have TS automatically infer T from the "multi" value:
const mySelect: MySelect = {
multi: true, // multi is true so T is true
onChange: (items) => {}
}
Important update: I want "multiple" to be optional (it would default to false if key is missing or undefined)
Upvotes: 1
Views: 1639
Reputation: 1074295
You've updated your question to say that multi
should be optional (default false). That rules out a discriminated union (the previous answer under the horizontal line below).
I think I'd use two interfaces you union together, and (if necessary) a base interface for the things they have in common. You'll probably want type guard functions for when you need to know the type of the select.
// Things all MySelects have in common (if you have anything other than `onChange`)
interface MySelectBase {
name: string;
}
// A single-select version of MySelect
interface MySingleSelect extends MySelectBase {
multi?: false;
onChange: (item: string) => void;
}
// A multi-select version of MySelect
interface MyMultiSelect extends MySelectBase {
multi: true;
onChange: (items: string[]) => void;
}
// The unified type
type MySelect = MySingleSelect | MyMultiSelect;
// Type guard function to see whether it's a single select
const isSingleSelect = (select: MySelect): select is MySingleSelect => {
return !select.multi; // !undefined and !false are both true
};
// Type guard function to see whether it's a multi select
const isMultiSelect = (select: MySelect): select is MyMultiSelect => {
return !!select.multi; // !!undefined and !!true are both true
};
Examples of creation:
const single: MySingleSelect = {
name: "some-single-select-field",
onChange : (item) => { console.log(item); }
};
const multi: MyMultiSelect = {
multi: true,
name: "some-multi-select-field",
onChange : (items) => { console.log(items); }
};
Example of using MySelect
(the combined interface):
const useMySelect = (select: MySelect) => {
// No need for a guard on anything but `onChange`
console.log(select.name);
// `onChange` will be a union type until/unless you use a type guard
const onChange = select.onChange;
// ^^^^^^^^−−−−−−−−−− type is `((item: string) => void) | ((items: string[]) => void)`
if (isSingleSelect(select)) {
// It's a MySingleSelect
const onChange = select.onChange;
// ^^^^^^^^−−−−−−−−−− type is `(item: string) => void`
} else {
// It's a MyMultiSelect
const onChange = select.onChange;
// ^^^^^^^^−−−−−−−−−− type is `(items: string[]) => void`
}
};
This is the original answer for folks who don't have the requirement of making multi
optional:
You can do that by declaring MySelect
as a union of types, one with multi: true
and the other with multi: false
:
type MySelect =
{
multi: true;
onChange: (items: string[]) => void;
}
|
{
multi: false;
onChange: (item: string) => void;
};
Then you get:
const mySelect: MySelect = {
multi: true,
onChange: (items) => {}
// ^^^^^^^^−−−−−−−−−−− correctly inferred as (items: string[]) => void
};
That's called a discriminated union: a union of types that are discriminated (told apart) by the type of one (or more) of the fields.
If you had a large set of other properties that didn't vary, you could add them to the discriminated union by using an intersection:
type MySelect =
(
{
multi: true;
onChange: (items: string[]) => void;
}
|
{
multi: false;
onChange: (item: string) => void;
}
)
&
{
the: number;
other: string;
properties: string;
};
Upvotes: 5
Reputation: 51034
You can do this using a generic identity function:
function helper<T extends boolean>(obj: MySelect<T>): MySelect<T> {
return obj;
}
// MySelect<true> inferred
const mySelect = helper({
multi: true,
onChange: (items) => {}
});
That said, this technique is more useful when your type parameter doesn't have a finite set of distinct options; in your case, T extends boolean
is probably better implemented as a tagged union type as in T.J. Crowder's answer, without generics.
Upvotes: 2