Jacob Conner
Jacob Conner

Reputation: 13

I am trying to create a dynamic function in typescript to update any property of an object

If I were to create a simple object such as the type below, I want to create a function that takes the object, a valid key of the object and a valid value based on the type of the value of the object's key.

export interface MyType {
    prop1: string;
    prop2?: number;
    prop3?: string[];
  }

I found I could easily type the update function to only allow valid properties for the type using the keyOf MyType method.

propertyName: keyOf MyType;

I also found I can type the value by accessing by accessing the type at the key. The code below should be a string.

propertyValue: MyType["prop1"];

However, I am having trouble typing propertyValue using all possible keys, prop1, prop2, and prop3.

export interface MyType {
    prop1: string;
    prop2?: number;
    prop3?: string[];
  }
  const instanceOfMyType: MyType = {
    prop1: "hello"
  }`

  const updatePropOfMyType = (thingBeingUpdated: MyType, 
    propertyName: keyof MyType,
    propertyValue: MyType[typeof propertyName]): MyType
    =>{
      const updatedType = _.cloneDeep(thingBeingUpdated);

      updatedType[propertyName] = propertyValue; 

      return updatedType; 
    };

Expected output:

const updatedInstanceOfMyType = updatePropOfMyType(instanceOfMyType, "prop2", 123);

console.log(updatedInstanceOfMyType)
// Expecting output {prop1: 'hello", prop2: 123}`

similarly

const anotherUpdatedInstanceOfMyType = updatePropOfMyType(instanceOfMyType, "prop2", "hi");
//should give me a type error since prop2 is a number not a string;

Actual output

Instead I have a type error "Type 'string | number | string[] | undefined' is not assignable to type 'never'. Type 'undefined' is not assignable to type 'never'." at the updatedType[propertyName] = propertyValue;

This appears to be due to how typescript is concatenating all possible types for the properties of MyType rather than filtering the type for the currently set value of PropertyName.

I am just trying to see if there is a way to dynamically enforce the type of the propertyValue to be the same as a given property based on the value of the currently set propertyName;

Upvotes: 1

Views: 133

Answers (1)

jcalz
jcalz

Reputation: 327624

If you want the compiler to keep track of the relationship between the particular argument passed in for propertyName and the type of propertyValue, you should make updatePropOfMyType generic in the type K of propertyName constrained to keyof MyType, as follows:

const updatePropOfMyType = <K extends keyof MyType>(thingBeingUpdated: MyType,
    propertyName: K,
    propertyValue: MyType[K]): MyType => {
    const updatedType: MyType = structuredClone(thingBeingUpdated)
    updatedType[propertyName] = propertyValue;
    return updatedType;
};

This will give you the call behavior you wanted:

const updatedInstanceOfMyType =
    updatePropOfMyType(instanceOfMyType, "prop2", 123); // okay
console.log(updatedInstanceOfMyType);
const anotherUpdatedInstanceOfMyType =
    updatePropOfMyType(instanceOfMyType, "prop2", "hi"); // error
// 'string' is not assignable to 'number' ------> ~~~~

Your original version doesn't work because the type of propertyName is the full union type keyof MyType and there is no "currently set" type the compiler can perceive, and so typeof propertyName is also the full union. By making the type of propertyName generic, you allow the compiler to narrow propertyName from keyof MyType to some specific literal type and then use it to narrow propertyValue as well.

Playground link to code

Upvotes: 1

Related Questions