4
votes

I am trying to create a function that takes in a key and a value and updates that corresponding key-value pair on a typed object. Here is a basic example of what I am after.

Link to code sandbox with below code

type x = {
    prop : number,
    other : string
}

let dataToUpdate : x = {
    prop: 1,
    other: "string"
}

function updateKey(key : string, value : number | string) {
    dataToUpdate[key] = value;
}

I am getting this error with the above implementation.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'x'. No index signature with a parameter of type 'string' was found on type 'x'.(7053)

Update I was introduced to the idea of an XY Problem. Below is the actual implementation I am after. I am trying to create a context within React that returns a function that allows you to only update a specific property within the object that is stored in the context

export function StoreProvider(props: any) {
    const [storeData, setStoreData] = useState<StoreData>(initProviderData);

    function setStoreKeyValue(keyToUpdate: keyof StoreData[], valueToSet: boolean | string | number | null){
        let newStoreData = {...storeData);
        const isStoreData = Object.keys(initProviderData).filter((key) => { key == keyToUpdate }).length > 1;

        if (isStoreData && newStoreData) {
            newStoreData[keyToUpdate] = valueToSet;
            setStoreData(newStoreData)
        }
    }

    return (
        <StoreContext.Provider value={{ "storeData": storeData, "setStoreData": setStoreData }}>
            {props.children}
        </StoreContext.Provider>
    )
}
2
First, there's no reason for the key to ever be a number.Emile Bergeron
I removed all irrelevant tags, but since you had listed reactjs, I guess this is an XY problem and you really should be asking about the real situation you're trying to solve instead of the roadblock you're facing with typings right now.Emile Bergeron
Nice edit! Next thing I see: newStoreData[keyToUpdate] is mutating the current storeData object, which is an anti-pattern as this state should stay immutable.Emile Bergeron
Quick JS tip: value={{ storeData, setStoreData }} is enough if the variable name and the key is the same. It's the Object initializer property shorthand.Emile Bergeron
That said, instead of a key and value params, you could consider an object param with the Partial<StoreData> type?Emile Bergeron

2 Answers

5
votes

TLDR;

updateKey<K extends keyof X>(key: K, value: X[K]) {
  dataToUpdate[key] = value; 
}

Details:

Given

type X = {
  prop: number;
  other: string;
};

const dataToUpdate: X = {
    prop: 1,
    other: "string"
};

TypeScript does not allow us to access properties of a well-typed object with a known set of properties, such as your dataToUpdate, via an arbitrary property key type such as string.

As the language knows that the only keys of dataToUpdate are "prop" and "other", the key parameter of your update function must be restricted accordingly.

While we could write out the union type explicitly in the parameter list, key: "prop" | "other", this pattern is so fundamental to JavaScript that TypeScript provides the keyof type operator which produces a union type of all the property keys of a given type, allowing us to write

updateKey(key: keyof X, value: string | number) {
  dataToUpdate[key] = value; 
}

However, we're not done yet because the language needs to ensure that the type of the value parameter corresponds to the property type of the specified key and, as it happens, "prop" must be a number and "other" must be a string.

To describe this we adjust our declaration as follows

updateKey<K extends keyof X>(key: K, value: X[K])

What we've done is associate the types of the two parameters such that the type of value matches the type of the property specified by key, whenever the function is called.

1
votes

The answer given by @Aluan is absolutely correct but there is another way also. Just add the dynamic key to your type as below:

type x = {
    prop : number,
    other : string,
    [key: string]: string | number;
}