1

I'm trying to make a typed flow of the assigning the JSON snapshot of the instance to the instance itself by setting the snapshot's values to the instance's properties. I used the keyof utility type to get the union type of the snapshot object keys. These keys are the same like the property names of the instance so it works without any problem in JS.
However at the stage of assigning values from the snapshot to the instance properties TS throws a weird error about the:

/** 
    Type 'string | boolean' is not assignable to type 'never'.
    Type 'string' is not assignable to type 'never'.ts(2322)
*/

I don't understand what's wrong here because keys should be the same or, maybe, I just missed something. Could someone, please, give to me some advice where did I made some mistake?

Here is the more detailed code example:


    export type AttachmentModelJsonType = {
      file: string,
      name: string,
      preview: string,
      uploading: boolean,
      optimistic: boolean,
      serverAttachmentId: string,
    };
    
    export class AttachmentModel {
      file: string;
      name: string;
      preview: string;
      uploading = false;
      optimistic = false;
      serverAttachmentId: string | null = null;
    
      updateFromJson = (data: AttachmentModelJsonType) => {
      
        (Object.keys(data) as Array<keyof AttachmentModelJsonType>).forEach(key => {
          // TS compiler throws an error about: 
          /** Type 'string | boolean' is not assignable to type 'never'.
              Type 'string' is not assignable to type 'never'.ts(2322)
          */
          this[key] = data[key];
        });
      };
    
    }

Thanks for any help!

P.S. Here is the code example where you can check this error:

Velidan
  • 5,526
  • 10
  • 48
  • 86
  • Please don't tag your titles. See [ask]. – isherwood Aug 28 '23 at 20:51
  • 2
    Did you mean `Object.assign`? – Dimava Aug 28 '23 at 21:01
  • 1
    (pls make sure your plaintext matches your link, `=""` etc) TS will only allow you to assign `T[K]` to itself if `K` is a *generic* type; if `K` is just a specific union type then [ms/TS#30769](https://github.com/microsoft/TypeScript/pull/30769) kicks in and makes you sad. So we can change the function to have a generic key and identical object type, as in [this approach](https://tsplay.dev/Wvyzkm). But something like `Object.assign()` is obviously easier. Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing? – jcalz Aug 28 '23 at 21:03
  • 1
    Seems like it would be easier to make `data` a `Partial` and assign it using `Object.assign(this, data)` – Heretic Monkey Aug 28 '23 at 21:05
  • 1
    this may solve your problem https://stackoverflow.com/a/68075914/8198889 – tsu Aug 28 '23 at 21:20
  • Thank you guys: you are helped me a lot. Yes, I could use `Object.assign` but I really wanted to understand why this error happened at the low level. @jcalz -> it's clear, thank you. I really must admit that due to TS specific the solution is much more complicated than the `assign` that handles this nuances automatically – Velidan Aug 28 '23 at 22:10

1 Answers1

0

TypeScript 3.5 introduced a feature, implemented in microsoft/TypeScript#30769, to be more strict about verifying assignments involving indexed access types, and since then it hasn't been possible to allow an assignment of the form o2[k] = o1[k] if o2 and o1 are compatible objects (say, both of type T) and k is a union type (say, of type KA | KB | KC). Right now what you get is that reading from o1[k] gives you a value of union type T[KA] | T[KB] | T[KC], but writing to o2[k] requires a value of intersection type T[KA] & T[KB] & T[KC]. These are rarely compatible with each other, and you get errors. There is a feature request at microsoft/TypeScript#32693 to add this support back.

Note that this strictness is desirable when dealing with o2[k2] = o1[k1], even if k1 and k2 are both of the same type KA | KB | KC. That's because maybe k1 and k2 will be different, and if they are, the only safe thing you could write to o2[k2] would be a value that works no matter what k2 is. Meaning T[KA] & T[KB] & T[KC]. Currently the compiler evaluates assignments based only on the types of the keys and not their identities, meaning it cannot tell the difference between o2[k2] = o1[k1] and o2[k] = o1[k]. So it complains about both of them, even though that latter is perfectly safe. Oh well, it's a missing feature, and you've run into it.


Currently the recommended workaround is to make the key generic type constrained to the union, instead of the union type itself. You are allowed to assign T[K] to itself when K is generic. So we can do this:

type AttachmentModelJsonKeys = keyof AttachmentModelJsonType;
type CommonAttachmentModelType =
    Pick<AttachmentModel, AttachmentModelJsonKeys>;

updateFromJson = (data: AttachmentModelJsonType) => {
    const _data: CommonAttachmentModelType = data;
    const _this: CommonAttachmentModelType = this;
    (Object.keys(data) as Array<AttachmentModelJsonKeys>).forEach(
        <K extends AttachmentModelJsonKeys>(key: K) => {
            _this[key] = _data[key]; // okay
        });
};

That works because both _this and _data are of type CommonAttachmentModeltype, and because key is of generic type K extends AttachmentModelJsonKeys. But yuck, that's a lot of boilerplate. You have to make the callback generic, and you have to widen this and data to the same type, just to get the obviously safe thing to work.


Instead if you're just trying to copy the properties from one object to another, you might as well use the JS-provided Object.assign(target, source) method. This method has essentially no type checking on target and source (see the library definitions), so the TS compiler will blithely let you copy any source to any target. Thus the following refactoring is nice and error-free:

updateFromJson = (data: AttachmentModelJsonType) => {
    Object.assign(this, data);
};

but so would this one:

updateFromJson = (data: AttachmentModelJsonType) => {
    Object.assign(this, window); // what?!
};

so you should be careful when using it. We've traded too-strict type checking for too-lax type checking.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360