1

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?

bozdoz
  • 12,550
  • 7
  • 67
  • 96
  • Interesting. I am wondering if this could ever work. As far as I know, TypeScript simply compiles to JavaScript. Any types, interfaces etc. are just for type checking purposes during compilation. But it seems to me you are trying to capture some run-time behavior in your type here. What would you actually expect to happen if `a.isMulti` will be changed somewhere else afterwards? TypeScript will not be able to check that, I guess. The emitted JavaScript code will be running, and it will not be aware of your original TypeScript constructs at all. – Bart Hofland Nov 26 '20 at 22:39
  • It's not really that interesting; using overloads is pretty common practice. I'm just curious as to how to get it to work with `undefined` as a type. **Note** in the typescript playground you can set isMulti to true or false and the onChange function is typed correctly. – bozdoz Nov 27 '20 at 00:48
  • setting `"strictNullChecks": true`, in tsconfig could do the job. While it's false both `isMulti: true` and `isMulti: false` can be assigned undefined. – Tomasz Gawel Nov 27 '20 at 02:56
  • strictNullChecks is on by default on TypeScript Playground – bozdoz Nov 27 '20 at 13:52
  • @bozdoz I have updated my answer, Please take a look, I believe this is what you are looking for – captain-yossarian from Ukraine Mar 12 '21 at 22:15

2 Answers2

1

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

  • The problem as I see it is that I want to know the value type in the definition of the variable `a`, which I see you've typed as `any` here. It's a neat solution however. Didn't think of trying to get `infer` to work for me. Thanks for the perspective; however, I need `onChange` to be aware of the sibling `options` type and the `isMulti` type. Surprised there doesn't seem to be a good way to do this. – bozdoz Mar 12 '21 at 19:18
  • This is possible to do with extra function. Are you agree on function overhead? – captain-yossarian from Ukraine Mar 12 '21 at 19:49
  • I tried separating the interfaces like in your example, and I've posted a solution. Take a look and hopefully you can improve it. :) – bozdoz Mar 12 '21 at 20:03
  • Second solution is a surprising number of "tricks", which I don't understand. That `onChange: (value: T)` is especially bizarre. Also makes the `onChange` functions useless: Try returning `value.value` or `value.length` instead of `value`: should be able to do one of those, but because it's typed as `T`, neither works. – bozdoz Mar 13 '21 at 20:57
  • @bozdoz my bad. But it is very easy to fix. I made a small update which helps you to understand this trick. Also I added constarints to generic to make it work as you expect – captain-yossarian from Ukraine Mar 13 '21 at 23:02
0

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)

bozdoz
  • 12,550
  • 7
  • 67
  • 96