'How to capture Typescript conditional property type

I have the following Typescript type:

type Config = {
  key: string,
  type: `input` | `switch` | `radios` | `select` | `checklist` | `image`,
  options: string[],
  value?: any,
}

I would like to be a bit more refined with my any type I set on the value property:

For example, I know that if the type is set to input, then value will be a string. If the type is set to switch, the value will be boolean

In a more complicated case, if type is radios, I know that value will be one of the values from the options array of strings.

Is there a way I can capture all of this?



Solution 1:[1]

To complement @Jared's answer and to solve your problem of passing the input string twice, we can make use of some meta programming and try to infer the type from the passed object literal using generic functions

type ValueMapping = {
    input: string;
    switch: string[];
    radios: number;
    // and so on
}


type Narrowable = number | string | boolean | undefined | symbol | void | null

declare function getConfig<N extends Narrowable,T extends {type: keyof ValueMapping} & {[k: string]: N | T | {} | []}>(x: T):
 EnforceConfigMapping<
  | T["value"] extends ValueMapping[T["type"]]
  ? true
  : false
>


type EnforceConfigMapping<T extends boolean> = 
 | T extends true
 ? 'All good' 
 : 'Type and value fields have incompatible type'


const good = getConfig({
    key: '',
    type: 'switch',
    options: [],
    value: ''
});


const bad = getConfig({
    key: '',
    type: 'input',
    options: [],
    value: 3
});


I can even make it to error, by adding some extra checks and but for now, we are displaying the appropriate msg as you hover.

Code Playground

Solution 2:[2]

You can do this with a mapped type and a generic:

// Tweak these to what your actual type mapping
// is, I used some rando types as an example.
// These can also be interfaces, doesn't
// matter in this case.
type ValueMapping = {
    input: string;
    switch: string[];
    radios: number;
    // and so on
}

type Config<T extends keyof ValueMapping> = {
  key: string,
  type: T
  options: string[],
  value?: ValueMapping[T],
}

const good: Config<'input'> = {
    key: '',
    type: 'input',
    options: [],
    value: ''
};

const bad: Config<'input'> = {
    key: '',
    type: 'input',
    options: [],
    value: 3 // Expected error
};

Playground

Solution 3:[3]

I would use a discriminated union here:

type BaseConfig = {
  key: string,
  options: string[],
};

type InputConfig = BaseConfig & {
  type: "input";
  value: string;
};

type SwitchConfig = BaseConfig & {
  type: "switch";
  value: boolean;
};

type Config = InputConfig | SwitchConfig | ... ;

Then you can choose to type a variable as InputConfig for example without the need for a generic helper function like in the other answers:

const config: InputConfig = {
  key: "foo",
  options: [],
  type: "input",
  value: "i like typescript",
};

Furthermore you can discriminate between configs with the type property:

if (config.type === "input") {
  // config.value is now string
}

Playground

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Bishwajit jha
Solution 2
Solution 3 hittingonme