bozdoz
bozdoz

Reputation: 12860

How to overload optional boolean in TypeScript

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)
}

See TypeScript Playground

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

Answers (2)

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

bozdoz
bozdoz

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

Related Questions