Search code examples
typescripttype-narrowing

TypeScript: type narrowing: assert not undefined and hence exclude from type


Both of these lines complain with error "Object possibly undefined" on _e[k]:

obj[k] = typeof _e[k] === "undefined" ? "" : _e[k].toString();
obj[k] = _e[k] === undefined ? "" : _e[k].toString();

_e[k] is a union of many types, therefore a manual type cast is not applicable. How do I get rid of the error?

update: So in a more general manner my question can be phrased as: is there a way to narrow the type of a variable by checking against specific types which should be excluded?

minimal example:

class test {
    a!: string;
    b?: number;
}
const t: test = { a: "hi", b: 2 };
for (const k of Object.keys(t) as Array<keyof test>) {
    console.log(t[k] == undefined ? "LOL" : t[k].toString());
}

Solution

  • There's a longstanding bug/issue whereby TypeScript does not perform control flow analysis to narrow the type of a property if the property key is a variable: microsoft/TypeScript#10530; it wasn't addressed because the fix imposed a large performance penalty. In your case that means type guard checks on _e[k] will not affect subsequent uses of _e[k].

    My recommended workaround for this is to assign your index-accessed property to a new variable and check against that:

    const ek = _e[k]; // new variable
    obj[k] = typeof ek === "undefined" ? "" : ek.toString(); // no error now
    

    The new variable ek gets normal control-flow type analysis applied, and your type guard works.

    Other possibilities for different use cases:

    • if you've already checked against undefined and null but the compiler can't figure it out, the easiest change is to use the non-null assertion operator !:

      obj[k] = typeof _e[k] === "undefined" ? "" : _e[k]!.toString(); // non-null assertion
      
    • You could refactor your type-exclusion code into its own self-contained expression so that you never have to re-check the original value. For example:

      obj[k] = (_e[k] || "").toString(); // refactor
      

      That particular code only works the only falsy values _e[k] can have are undefined and "". If _e[k] could be 0 then that would give a different result. In general you can make a new function to do whatever you want:

      function defined<T, U>(x: T | undefined, dflt: U): T | U {
          return typeof x !== "undefined" ? x : dflt;
      }
      obj[k] = defined(_e[k], "").toString(); // refactor 2
      

    Okay, hope one of those helps; good luck!

    Playground link to code