-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
Suggestion
🔍 Search Terms
typeguard narrow infer implicit
✅ Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
Some method for writing a typeguard that narrows based on Typescript’s regular type-narrowing rules, without having to specify the result of that narrowing. Syntax might be as narrowed, as in (x) => x is as narrowed. Since as is already a reserved TS keyword that must follow an expression, and cannot follow is, there is no possibility of this syntax being confused for any other valid TS expression. And x is is the already-implemented syntax for typeguards, keeping things consistent.
The goal with this syntax would be to write if (typeguard(x)) and have it behave exactly as if I had copied the body of typeguard into the if and replaced its arguments with x.
📃 Motivating Example
function isFooBarBaz(x: Foo): x is as narrowed {
return typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined;
}💻 Use Cases
The motivation is, currently we can write:
declare function setFooBarBaz(n: number): void;
interface Foo {
foo?: number | string | {
bar?: number | string | {
baz?: number
};
};
}
function maybeUpdateFooBarBaz(x: Foo) {
if (typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined) {
setFooBarBaz(x.foo.bar.baz);
}
}But if we want to make typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined re-usable, we have to define a custom typeguard:
function isFooBarBaz(x: Foo): x is { foo: { bar: { baz: number } } } {
return typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined;
}
function maybeUpdateFooBarBaz(x: Foo) {
if (isFooBarBaz(x)) {
setFooBarBaz(x.foo.bar.baz);
}
}In addition to being verbose, this isn’t precisely typesafe—if the typeguard returns true, Typescript just assumes that x has this type, it doesn’t actually check that our typeguard has ensured that (beyond basic assignability checks to ensure that it is possible that x might have this type). In other words, it is my responsibility to ensure that if (isFooBarBaz(x)) is equivalent to the narrowing that Typescript does for me when I write if (typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined).
The problem with that is, if I update the definition of Foo to allow the possible baz property to have the type string | number, but forget to update isFooBarBaz, Typescript will accept the assertion that baz is a number even though I only checked it was !== undefined, and it could be a string. I want a typeguard where the compiler would catch this, just as it does for the non-typeguard version.
Thus the proposal:
function isFooBarBaz(x: Foo): x is as narrowed {
return typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined;
}
function maybeUpdateFooBarBaz(x: Foo) {
if (isFooBarBaz(x)) {
setFooBarBaz(x.foo.bar.baz);
}
}By using x is as narrowed, my if(isFooBarBaz(x)) is actually identical to the original if (typeof x === 'object' && typeof x.foo === 'object' && typeof x.foo.bar === 'object' && x.foo.bar.baz !== undefined), and Typescript is handling its type in exactly the same way. I don’t have to determine (and maintain) the type signature of isFooBarBaz.