PaulParton
PaulParton

Reputation: 1033

Typescript function with an argument that gets its keys from another argument

I want to create a function that accepts two arguments, the first is an object that includes an array of strings that are the required fields for the second argument. e.g

const configOptions = {
    config1: {
        value: string,
        props: ['firstProp', 'secondProp']
    }
};

myFunc(configOptions.config1, {
  'firstProp': 'first value', // <-- firstProp and secondProp are required because they are defined in config.props
  'secondProp': 'second value',
});

I can achieve this by referencing the specific config in the function declaration and doing a const assertion on the config options


const configOptions = {
  config1: {
      value: 'a string value',
      props: ['firstProp', 'secondProp']
  }
} as const;

function myFunc<T extends typeof configOptions.config1>(
  option: T,
  params: { [key in typeof option.props[number]]: string }
) {}

myFunc(configOptions.config1, {
  firstProp: 'first value',
  secondProp: 'second value',
});


But I want to achieve this more generically so I could call the same function and use one of many configs. Is this possible?

Here is a stackblitz https://stackblitz.com/edit/typescript-ydyt4v

Upvotes: 2

Views: 818

Answers (1)

skovy
skovy

Reputation: 5650

Using a generic with a constraint will allow inferring the type of the config. This inferred generic can then be used to define the type for the second argument.

const configOptions = {
    config1: {
        value: 'string',
        props: ['firstProp', 'secondProp']
    },
    config2: {
        value: 'string',
        props: ['one', 'two']
    }
} as const;

// Define the base shape (most general shape) of the config. 
// I used the const assertion (`as const`) above so defining the props as 
// readonly made this work nicely but that could be changed.
interface BaseConfig {
    value: string;
    props: readonly string[]
}

const myFunc = <
    // Define a generic `Config` that adheres (extends) the base shape (BaseConfig).
    // It represents the first argument and is inferred from the passed `config`.
    Config extends BaseConfig
>(
    config: Config,
    // Define the input values type based on the passed `Config`.
    // Specifically, we want the values of the props key.
    // And those keys are for a record, with a string value.
    values: Record<Config["props"][number], string>
) => {
    // TODO: implement as needed.
    return null;
}

// Okay.
myFunc(configOptions.config1, {
    'firstProp': 'first value',
    'secondProp': 'second value',
});

// Property 'secondProp' is missing in type.
myFunc(configOptions.config1, {
    'firstProp': 'first value',
});

// Okay.
myFunc(configOptions.config2, {
    'one': '1',
    'two': '2'
});

// Property 'one' is missing in type.
myFunc(configOptions.config2, {
    'two': '2'
});

TypeScript playground link

Upvotes: 3

Related Questions