5
interface modal {
    name: string;
    check: boolean;
}

type Key = "name" | "check";

const obj: modal = fn();

function fn():any {
    return {
        name: "hello"
    }
}

class Modal {
    name: string;
    check: boolean;
    constructor() {
        this.name = "";
        this.check = false;
        Object.keys(obj).forEach((key: string) => {
            this[key as keyof modal] = obj[key as keyof modal];
        })
    }
}

I get an error at this[key as keyof modal]

Error message: Type 'string | boolean' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'. enter image description here

Willy
  • 145
  • 3
  • 8
  • Inside the `Modal` class, `keyof modal` won't allow you to index into a `Modal` object even though it happens to have the same keys as `modal`, an unrelated interface. You need `keyof Modal`. – FZs Jan 18 '22 at 09:42
  • You mean change to this[key as keyof Modal] instead of this[key as keyof modal]? Unfortunately, it does not work neither – Willy Jan 18 '22 at 09:51

1 Answers1

4

Object.keys always return string[], no matter what. Of course, it is expected that it returns Array<keyof typeof obj> in this case, but it is not true. Please see this list of issues on github.

First thing that should be done in this case is type assertion.

(Object.keys(obj) as Array<keyof typeof obj>)

However, this is not the end. There is still an error here:

this[key] = obj[key]; // error

In general, TS does not like mutations. Please see my article and this answer.

Type of this[key] and obj[key] is string | boolean.

Please see this code:

type Key = "name" | "check";

let _name: Key = 'name'
let _check: Key = 'check'
obj[_name] = obj[_check] // error

Above code is almost equal to yours, except, your mutation is inside of iterator and mine is not. There is no binding between iteration index and type of key.

See example:

(Object.keys(obj) as Array<keyof typeof obj>)
  .forEach((key, index) => {
    if (index === 0) {
      const test = key // keyof modal and not "name"
    }
  })

It is correct behavior, because even JS specification does not give you a guarantee that first key is name. JS engine reserves the right to return you keys in any order. Sure, in 99.99% cases you will get the expected order, but it does not mean that you have a guarantee.

So, why do we have never in the error message ? TypeScript makes an intersection of expected keys (of union) because it is safer to get common type. Intersection of string & boolean - gives you never, this is why you have this error message.


I believe the best you can do without using type assertions is to call reduce:

interface modal {
  name: string;
  check: boolean;
}

type Key = "name" | "check";

const obj: modal = fn();

function fn(): any {
  return {
    name: "hello"
  }
}

class Modal implements modal {
  name: string;
  check: boolean;
  constructor() {
    this.name = "";
    this.check = false;
    const result = (Object.keys(obj) as Array<keyof typeof obj>)
      .reduce((acc, key) => ({
        ...acc,
        [key]: obj[key]
      }), this)
    Object.assign(this, result)
  }
}

Playground It worth using implements modal or capitalize modal interface.