Gyan
Gyan

Reputation: 528

Type 'string | number | null | undefined' is not assignable to type 'null'

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'. */
    }
  }

Upvotes: 2

Views: 2215

Answers (1)

Terry
Terry

Reputation: 66093

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.

Upvotes: 3

Related Questions