2

I would like to implement an interface that could be seen like this:

interface SelectProps {
  options: Array<{ value: string; text: string }>
  currentValue: string
}

But I would like to improve it so the currentValue property has to be one of the options' value.

I was thinking of doing something like this:

interface Props<T extends string> {
  options: Array<{ value: T; text: string }>
  currentValue: T
}

But with that solution I would have to use that interface like this:

const props: Props<'foo' | 'bar' | 'baz'> = {
  options: [
    { value: 'foo', text: 'Foo' },
    { value: 'bar', text: 'Bar' },
    { value: 'baz', text: 'Bar' }
  ],
  currentValue: 'foo'
}

But I would like T to be automatically inferred from options instead:

const props: Props = {
  options: [
    { value: 'foo', text: 'Foo' },
    { value: 'bar', text: 'Bar' },
    { value: 'baz', text: 'Bar' }
  ],
  currentValue: 'zzzz' // Error, no in options
}

How could that be done?

Thanks

FlorianB
  • 179
  • 1
  • 1
  • 11
  • Do you want `value` to be `string` or `{ value?: string; text?: string }` ? – Ashok Mar 31 '22 at 15:19
  • 1
    This is a near-duplicate of [this question](https://stackoverflow.com/q/71285658/2887218). There is no specific type that represents your constraint; you could make it generic and use a helper function like [this](https://tsplay.dev/WG5e9N), but that's probably the best you can do. – jcalz Mar 31 '22 at 15:34
  • I have updated the question to avoid confusion on what I wanted to result to be like – FlorianB Mar 31 '22 at 16:09
  • My answer would still be the same; there is no specific type that works this way, you need generics, and therefore a helper function to infer the generic instead of asking you to specify it. Please look at [this approach](https://tsplay.dev/wgX9MN) and tell me if that works for you or if I'm missing something about the question. – jcalz Mar 31 '22 at 16:18

1 Answers1

1

Simple with helper type

type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]

interface Options {
    value: string;
    text: string;
};

interface SelectProps {
    options: Array<Options>;
    value: AtLeastOne<Options>;
}

const foo: SelectProps = {
    options: [{ text: '', value: '' }],
    // value: { text: '' }  // Ok
    // value: { value: '' } // Ok
    // value: {  } // Err
}

We can also create AtMostOne ExactlyOne

type Explode<T> = keyof T extends infer K
  ? K extends unknown
  ? { [I in keyof T]: I extends K ? T[I] : never }
  : never
  : never;

type AtMostOne<T> = Explode<Partial<T>>;
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
type ExactlyOne<T> = AtMostOne<T> & AtLeastOne<T>

interface Options {
    value: string;
    text: string;
};

interface SelectProps {
    options: Array<Partial<Options>>;
    value: ExactlyOne<Options>;
}

const foo: SelectProps = {
    options: [{ text: '', value: '' }],
    // value: { text: '' }  // Ok
    // value: { value: '' } // Ok
    // value: { value: '', text: '' } // Err
    // value: {  } // Err
};

Great post about these types, the @grahamaj answer.

Filip Seman
  • 1,252
  • 2
  • 15
  • 22