2

I am trying to implement C# Class object initializer in typescript class. But compiler showing following error.

error : Type 'string | number | null | undefined' is not assignable to type 'null'. Error : Type 'undefined' is not assignable to type 'null'

Below is the code

type gender = 'Male' | 'female';

class Person {
    public name: string | null = null;
    public age: number | null = null;
    public gender: gender | null = null;

    constructor(
        param: Partial<Person>
    ) {
        if (param) {

            /**
             * Is key exist in Person
             * @param param key
             */
            const keyExists = (k: string): k is keyof Person => k in this;

            /**
             * Keys
             */
            const keys: string[] = Object.keys(param);

            keys.forEach((k: string) => {
                if (keyExists(k)) {
                    //k: "name" | "age" | "gender"

                    const value = param[k]; //value: string | number | null | undefined

                    //let a = this[k]; //a: string | number | null

                    this[k] = value; //error :  Type 'string | number | null | undefined' is not assignable to type 'null'.
                                    // Error :  Type 'undefined' is not assignable to type 'null'.
                }
            });

        }
    }

}

let a = new Person({
    age: 10
});

console.log(a);

and below is the tsconfig.json

{
    "compilerOptions": {
      "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
      "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
      "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
      "strict": true,                           /* Enable all strict type-checking options. */
      "strictNullChecks": true,              /* Enable strict null checks. */
      "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
      "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    }
  }
1
  • 2
    The error is right but misleading. The problem is that this[k] is a union of all possible properties, so it accepts number | string | null but the only thing you can assign that fits into every category is null. It makes sense if you think of it this way k might be name or age - you know it's one but not sure which. Then value might be a string or a number - again, not sure which one. If value = 42 + k = "name" you'll try to assign a number to a string. That doesn't work. value = "Fred" + k = "age" also doesn't work. The only type that satisfies all is null. Commented May 22, 2020 at 5:22

1 Answer 1

3

Your problem is actually two fold:

  1. param is Partial<Person>, therefore it may not contain all the keys present. Yet, the keyExists() method will only inform the compiler if a given key is present in Person, so you will end up with param[key] returning undefined.
  2. As @VLAZ has pointed out in the comments, the compiler cannot intelligently guess, or narrow the type, of value down to a key-by-key basis.

The solution might not be the best, but it's what that will work:

  1. You need to add a type guard check if param[k] is undefined
  2. You need to manually cast the type of this[k] to typeof value

Even though I usually avoid using manual type casting, in this case it's quite reasonable and safe, because you are, at the point of casting, working with k that must be a member of the Person class, so you are simply hinting the compiler that "I know what specific type I am working with, don't panic".

With that, the modified logic inside the Array.prototype.forEach callback looks like this:

keys.forEach((k: string) => {
  if (keyExists(k)) {
    const value = param[k];

    if (value === void 0)
      return;

    (this[k] as typeof value) = value;
  }
});

See it on TypeScript Playground.

Personally I don't like nested if statements because it makes the code really difficult to read, so you can also refactor the above code to use guard clauses exclusively:

keys.forEach((k: string) => {
  if (!keyExists(k))
    return;

  const value = param[k];

  if (value === void 0)
    return;

  (this[k] as typeof value) = value;
});

See on TypeScript Playground.

Sign up to request clarification or add additional context in comments.

5 Comments

(this[k] as typeof value) = value; ah, of cousre! I was wracking my brain trying to figure out how would you generically ensure that value is assignable to the property k but I forgot that you are already ensured compatibility from accepting a Partial<Person>. Therefore value must match the type for the property it was assigned to. It's just the compiler that doesn't infer that.
@VLAZ Your comment did an excellent job at explaining why this[k] will only accept null value: I am at a loss of words to explain why and your comment explained it in a way that's digestible :)
My secret is not being very good with types. So, I try to figure out the combinations. Then I realised k = "name" and value = 42 don't fit together. Yet, like the compiler I was missing the link that value already comes from a property with a type, so if you can't actually have that situation as Partial<Person> wouldn't allow you to use {"name" : 42 }
@Terry (this[k] as typeof value) = value; just worked for me. But it not the solution i am looking for. Because undefined is not a common type between Partial<Person> and this.
@Gyan if (value === void 0) return; will filter out undefined, so value cannot be that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.