How to type function parameters in TypeScript to map object properties?

Viewed 42

I am trying to understand how to correctly set the types to the prop and value function parameters (see the ??) so that prop and value must match the ones defined in the MyStorage interface. Setting keyof MyStorage should be sufficient to correctly type prop, but then I'm completely lost on how to type value.

interface MyStorage {
  prop1?: string;
  prop2?: number;
  prop3?: boolean;
  prop4?: Array<string>;
  prop5?: 'value1' | 'value2';
}

const myStorage: MyStorage = {};

const setProp = (prop: ??, value: ??) => {
  myStorage[prop] = value;
}

setProp('prop1', 'Ok');
setProp('prop1', 5); // Not Ok
setProp('prop5', 'value1'); // Ok
setProp('prop5', 'value5'); // Not Ok

The ultimate goal is to transform this function to a generic version like this:

const genericSetProp = <T>(prop: ??, value: ??, storage: T) => {
  storage[prop] = value;
}

genericSetProp('prop1', 'Ok', myStorage);
genericSetProp('prop1', 5, myStorage); // Not Ok
genericSetProp('prop5', 'value1', myStorage); // Ok
genericSetProp('prop5', 'value5', myStorage); // Not Ok

How can I set those types to make TypeScript correctly check those parameters?

2 Answers

You can use the type of a member of a structure by doing MyStorage["prop1"] etc. To get the "prop1" part, you can require that key be keyof MyStorage. So the first version looks like this:

const setProp = <K extends keyof MyStorage>(prop: K, value: MyStorage[K]) => {
    myStorage[prop] = value;
};

And you can indeed make that generic by having the type MyStorage inferred from a third argument, using a second generic type parameter:

const genericSetProp = <T, K extends keyof T>(prop: K, value: T[K], storage: T) => {
    storage[prop] = value;
};

Playground link

Just use extra generic:

interface MyStorage {
  prop1?: string;
  prop2?: number;
  prop3?: boolean;
  prop4?: Array<string>;
  prop5?: 'value1' | 'value2';
}

const myStorage: MyStorage = {};

const setProp = <Prop extends keyof MyStorage,>(prop: Prop, value: MyStorage[Prop]) => {
  myStorage[prop] = value;
}

setProp('prop1', 'Ok');
setProp('prop1', 5); // Not Ok
setProp('prop5', 'value1'); // Ok
setProp('prop5', 'value5'); // Not Ok

FP approach:

const withObject = <Obj,>(obj: Obj) => <Prop extends keyof MyStorage>(
  prop: Prop, value: MyStorage[Prop]
) => ({
  ...obj,
  [prop]: value
})

const setProp = withObject(myStorage)

setProp('prop1', 'Ok');
setProp('prop1', 5); // Not Ok
setProp('prop5', 'value1'); // Ok
setProp('prop5', 'value5'); // Not Ok

Playground

Related