Reputation: 12860
I'm trying to infer some types for working with a combobox component.
Basically, given a type T
, the options should be T[]
, and an onChange handler should be (value: T) => void
. However, if the isMulti
flag is true, then onChange should be (value: T[]) => void
. I'm just unsure as to how to configure the overloading types to get this to work properly:
type Options<T = any> = {
options: T[]
} & ({
isMulti?: false
onChange: (value: T) => void
} | {
isMulti: true
onChange: (value: T[]) => void
})
interface Option {
value: string,
}
const a: Options<Option> = {
// isMulti: false,
options: [{
value: 'abc',
}],
onChange: (value) => console.log(value)
}
Basically the issue is that if isMulti
is undefined, then the value in onChange is any
!
Parameter 'value' implicitly has an 'any' type.
Is there any way to do this, or do I need to make isMulti
required?
Upvotes: 1
Views: 1064
Reputation: 33071
It turned out that we had to add extra generic to onChange
method with constraint:
type A<T> = {
isMulti: false,
onChange: (value: T) => any
}
type B<T> = {
isMulti: true,
onChange: (value: T[]) => any
}
type C<T> = {
onChange: (value: T) => any
}
// credits goes to Titian Cernicova-Dragomir
//https://stackoverflow.com/questions/65805600/struggling-with-building-a-type-in-ts#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type Unions<T> = StrictUnion<A<T> | B<T> | C<T>>
type Options<T = any> = {
options: T[]
} & Unions<T>
interface Option {
value: string,
}
const a: Options<Option> = {
options: [{
value: 'abc',
}],
// trick is here
onChange: <T extends Option>(value: T) => {
return value
}
}
const b: Options<Option> = {
isMulti: true,
options: [{
value: 'abc',
}],
onChange: <T extends Option[]>(value: T) => {
return value
}
}
const c: Options<Option> = {
isMulti: false,
options: [{
value: 'abc',
}],
onChange: <T extends Option>(value: T) => {
return value
}
}
// error because if isMulti is false, value should be Option and not an array of Option
const d: Options<Option> = {
isMulti: false,
options: [{
value: 'abc',
}],
// should be T extends Option instead of T extends Option[]
onChange: <T extends Option[]>(value: T) => {
return value
}
}
a.onChange({ value: 'hello' }) // ok
b.onChange([{ value: 'hello' }]) // ok
c.onChange({ value: 'hello' }) // ok
c.onChange([{ value: 'hello' }]) // expected error
If you want to understand this trick, please read answer
Upvotes: 1
Reputation: 12860
I may have a workaround, though I don't know if it's the best solution. I tried explicit types for each scenario and defined an overloaded function to pass the object through (though I would love to have this work without the impractical function):
interface Option {
value: string,
}
// default Options
type Options<T = Option> = {
options: T[]
}
// isMulti is undefined
type U<T> = {
onChange: (value: T) => void
}
// isMulti is false
type F<T> = {
isMulti: false
} & U<T>
// isMulti is true
type M<T> = {
isMulti: true
onChange: (value: T[]) => void
}
// overloads for determining given parameters
function getOptions<T>(options: Options<T> & M<T>): typeof options;
function getOptions<T>(options: Options<T> & U<T>): typeof options;
function getOptions<T>(options: Options<T> & F<T>): typeof options;
function getOptions(options: any) {
return options;
}
const a = getOptions({
// isMulti: false,
options: [{
value: 'abc',
}],
onChange: (value) => console.log(value)
})
This works as I would hope, but it still seems like overkill, and I'm not sure if this works for props for a JSX/React component, for example.
View on typescript playground (link)
Upvotes: 0