Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript: programmatically updating multiple object fields in produce causes issues with index signature #1002

Closed
2 tasks
floroz opened this issue Dec 14, 2022 · 4 comments
Labels

Comments

@floroz
Copy link

floroz commented Dec 14, 2022

馃檵鈥嶁檪 Question

I am unable to generate a correct type signature for a setter function that programmatically updates multiple fields in an Object Draft.

I am looking for advice on how to combine TypeScript and Immer to create a correct signature (hopefully without using casting as).

Link to repro

I created a TypeScript playground where you can see the error reproduced.

Environment

"typescript": "~4.8.2"
"immer": "^9.0.16",

  • Immer version: "^9.0.16"
  • Occurs with setUseProxies(true)
  • Occurs with setUseProxies(false) (ES5 only)
@childrentime
Copy link
Contributor

draft[key] = value!

@floroz
Copy link
Author

floroz commented Jan 7, 2023

draft[key] = value!
Thanks.

The bang! satisfies the compiler, I guess my question is more why is this necessary and if there is a way to have the correct type inference/narrowing without reaching to compiler flags directly.

@BenceSzalai
Copy link

BenceSzalai commented Mar 4, 2023

This is the way TypeScript is. Partial<TState> is not a known type, it is rather the union of all possible types that can be imagined by setting any number of properties in TState to hold undefined. So despite the fact that in your code a human observer can deduce that partial[key] will always be defined, TypeScript is "thinking" in terms of types, and all it can know about key is that it is any possible key of Partial<TState>, and because any Partial<TState> includes all possible partials, keyeof Partial<TState> is exactly the same as keyeof TState.

So when your code goes like this:

/*...*/ partial: Partial<TState> /* ... */
for (let key in partial) {
/* ... */

TypeScript is not having any particular Partial<TState> in mind that is the exact type of partial. It is still just the union of TState and TState with any one property being undefined and TState with any two properties being undefined etc.

So when you later do:

const value = partial[key];

value becomes Partial<TState>[Extract<keyof TState, string>] and not something like Partial<TState>[Extract<keyof {that particular given and known configuration out of the hundreds of possible Partial<TState> varieties that is the exact type of the partial variable during runtime as declared few lines above), string>].

This is because TypeScript is not analysing the code to the level to be able to guarantee that key can only be a valid key of the exact given partial here. It is "thinking" in terms of types. And in terms of types all it can say about key is that it will be a valid (string) key of TState. But indeed any key of a TState is not guaranteed to exist on Partial<TState>. In fact Partial<TState> can be also fulfilled by an object on which none exist. And in fact when you access Partial<TState> using a key of TState, your value may be undefined.

And there is the issue you are facing: you cannot assign undefined to draft[key].

This is a very valid case for using !, because in this context you know something about key that the compiler does not, namely that here key is produced in a way that it'll have exactly those keys, which are present in partial. The compiler does not know this, because on the Types level it is not guaranteed. It is only a coincidental guarantee based on the way your code behaves during Run-Time.

If you don't like ! you can do other things, like adding type guards to ensure it is not undefined or simply "throwing" an exception that is never going to be thrown:

const value = partial[key] ?? (()=>{throw new Error('Logic error, impossible situation, partial cannot have undefined under any key of partial.')})();

At least this way you know that you have a runtime guarantee as well. So if someone makes some changes to the code, and partial[key] ends up being undefined at least the issue will be apparent where it happens, instead causing something else somewhere else to fail, like it would when forcing TSC to accept value as not undefined using !.

@floroz
Copy link
Author

floroz commented Mar 5, 2023

Thank you @BenceSzalai for the thorough explanation :)

It seems that this cannot be resolved from the Immer side, so I'll close the issue 馃憤

@floroz floroz closed this as completed Mar 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants