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'.
}
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 // ensure not nullish.
&& (typeof obj == "object" || typeof obj == "function") // ensure not primitive.
&& propName.every(x => x in obj); // ensure every prop is 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With