6

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>
    )
}
  • 1
    First, there's no reason for the `key` to ever be a number. – Emile Bergeron Jul 20 '20 at 02:31
  • 1
    I removed all irrelevant tags, but since you had listed reactjs, I guess this is an [XY problem](https://meta.stackexchange.com/q/66377/254800) 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 Jul 20 '20 at 02:36
  • @EmileBergeron That's a fair point. Thanks for sharing that, I have updated the post with the actual implementation within React that I am trying to solve – Shawn Shellenbarger Jul 20 '20 at 02:53
  • 1
    Nice edit! Next thing I see: `newStoreData[keyToUpdate]` is [mutating the current `storeData` object](https://stackoverflow.com/q/43638938/1218980), which is an [anti-pattern as this state should stay immutable](https://stackoverflow.com/a/37760774/1218980). – Emile Bergeron Jul 20 '20 at 03:04
  • 1
    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](https://stackoverflow.com/q/34414766/1218980). – Emile Bergeron Jul 20 '20 at 03:07
  • 1
    That said, instead of a `key` and `value` params, you could consider an object param with the [`Partial` type](https://stackoverflow.com/q/36633639/1218980)? – Emile Bergeron Jul 20 '20 at 03:13
  • 1
    Thanks! I was trying to avoid modifying the storeData object, but realize now that I was! Using the spread operator seems to be a better way of cloning the object. I'll give Partial a go and see what I can come up with. Thanks for all of the help! – Shawn Shellenbarger Jul 20 '20 at 03:17
  • 1
    just because I don't see mentioned, the correct signature of such a function is `updateKey(key: K, value: x[K])` – Aluan Haddad Jul 20 '20 at 03:51
  • @AluanHaddad Thank you! Please post your answer so I can mark it as the best answer! – Shawn Shellenbarger Jul 20 '20 at 04:03

2 Answers2

9

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.

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
1

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;
}
Shivang Gupta
  • 3,139
  • 1
  • 25
  • 24
  • 1
    While this technically solves for the type x case, it is not as extendable when you start working with more complex types. In my case, I have a type with several keys that all vary in type, so you can end up essentially allowing for an implicit type of `any` in this case. @Aluan Haddad's response allows for any number & type of keys without losing the strict typing of your parameters – Shawn Shellenbarger Jul 20 '20 at 12:21
  • 1
    I agree to you Shawn. This even works with required fields only. – Shivang Gupta Jul 20 '20 at 12:36