Type Predicates on Properties
I fell down a brief rabbit hole this morning while attempting to refactor some TypeScript code.
I had some code along the lines of
if (this.foo.bar?.baz === 'example') {
that was repeated in a variety of locations.
I wanted to refactor it to
if (this.isExample) {
but one of the nuances of that refactor is that the first version includes an implicit type predicate for this.foo.bar
to not be undefined, because this.foo.bar
must be set for .baz
to equal 'example'
.
The code blocks that followed sometimes referenced properties of this.foo.bar
, and so in "simplifying" the checks, I'd need to use a non-null assertion operator:
if (this.isExample) {
this.foo.bar!.fizz = 'buzz';
// ^ - 😝
}
I greatly dislike using the non-null assertion operator because it's a recipe for bugs during refactors. Things that seem like they couldn't ever be undefined suddenly change and cause un-handled errors.
From digging, it doesn't appear that properties are allowed to have type predicates at present—which is unfortunate. It would be really convenient for isExample
to be defined as:
get isExample(): this.foo.bar is Bar {
return this.foo.bar?.baz === 'example';
}
Since properties don't support type predicates, the next best thing is to use a function:
isExample(): this.foo.bar is Bar {
return this.foo.bar?.baz === 'example';
}
Unfortunately, type predicates don't allow you to specify property types directly from this
.
The trick to get around this limitation is to create an interface or nested type for the predicate:
// interface version
interface HasBar {
foo: {
bar: Bar
}
}
isExample(): this is HasBar {
// nested type version
isExample(): this is { foo: { bar: Bar } } {
Having learned all this, it certainly feels like something that TypeScript could handle better.
It also makes me wonder about cases where one might want multiple type predicates on a method or function—but on further reflection those cases are probably attempting to do too much in a single function call.