TypeScript error handling an unknown variable: Property does not exist on type 'object'

Viewed 3725

I'm trying to understand how to properly approach handling an unknown variable, which may be an object with certain properties I can use. This derives from a variety of try/catch scenarios when working with a third party library which is still in development, so "e" may or may not be an object with certain properties.

Below is a simplified version of my question, but there are other manifestations of the issue. Therefore, I wish to avoid type assertions or disable type checking. What I'm looking for is a proper way to check an "unknown" value to see if it is an object with certain properties.

Why is the following if condition insufficient? TypeScript just views it as an unknown object and isn't use the info I've passed to ensure (1) it is an object, (2) it is not null, and (3) it has the key "errNum".

What other possible condition could cause "console.log(e.errNum)" to throw an error at runtime after all of these checks? This fails both in the VSCode GUI and when building with webpack 5 (using eslint after recent migration from tslint). I can't think of any other edge cases that would cause problems at runtime.

try {
    const err = { errNum: 12 };
    throw err;
} catch (e) {
    // e is inferred as unknown, as it should be
    if (e && typeof e === 'object' && 'errNum' in e) console.log(e.errNum); // TS error: Property 'errNum' does not exist on type 'object'.
}
3 Answers

You will not be able to take advantage of the type checker's inference capabilities because the catch clause variable is unknown. In this case the easier things might be to use the condition to determine that errNum is property in a type guard function.

catch (e)
{
  if (isErrorNumObj(e)) console.log(e.errNum);
}

function isErrorNumObj<T>(obj: T): obj is T & { errNum: unknown } {
  return obj && 'errNum' in obj;
}

You can even make a more generic version that can accept any number of props.

try {
  const err = { errNum: 12, stackTrace: 'abc' };
  throw err;
} catch (e: unknown) {

  if (hasProps(e, 'errNum', 'stackTrace')) {
    console.log(e.errNum, e.stackTrace);
  }
}

function hasProps<T, U extends string | number | symbol>(obj: T, ...propName: U[]): obj is T & { [P in U]: unknown } {
  return obj && propName.every(x => x in obj);
}

I'd create a user defined type predicate.

function isErrNum(x: any): x is { errNum: number } {
    return !!x && 'errNum' in x;
}

try {
    const err = { errNum: 12 };
    throw err;
} catch (e: unknown) {
    // e is inferred as unknown, as it should be
    if (isErrNum(e)) {
        console.log(e.errNum);
    }
}

We extract our "test" code into a function that returns "variable is type". And a "true" return then informs the compiler that in the context of that being true, the object is that declared type and is not that type in an else.

TypeScript Playground

In typescript the in operator can be used as a type guard, to allow the compiler to narrow down a type in a union type: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

However, in your case as e is inferred to be of object type which is not a union type, so even after applying errNum in e, it remains an object type instead of being inferred as {errNum: number}

What you could do instead is define a custom error class like so:

class MyError extends Error {
  constructor(readonly errNum: number) {
    super()
  }
}

use it to construct your error:

throw new MyError(10)

then narrow its type using instanceof: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#instanceof-narrowing

catch (e) {
  if (e instanceof MyError) {
    console.log(e.errNum)
  }
}
Related