Description
openedon Sep 12, 2022
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
.