User-defined type guards in TypeScript
2021-03-09
Decided to write this blog post because I couldn’t find any documentation or online examples like the one I’m sharing here. Every example I’ve seen the user-defined type guard is used to replace the whole type definition and not specific properties.
Here’s the code without type guard.
interface Config {
foo: string;
bar: string;
}
class Thing {
name: string;
config?: Config;
constructor(name: string, config?: Config) {
this.name = name;
this.config = config;
}
// 👀 returning boolean, will change on next example
hasConfig(): boolean {
if (this.config) {
return true;
}
return false;
}
}
let firstThing = new Thing("Apple", { foo:"one", bar: "two" });
if (firstThing.hasConfig()) {
// #1 - Here firstThing.config is `Config | undefined`
console.log(firstThing.config.foo);
}
if (firstThing.config) {
// #2 - Here firstThing.config is `Config`
console.log(firstThing.config.foo);
}
On #1 firstThing.config
. is Config | undefined
even
though we know it can’t be undefined
because our
hasConfig
method made sure the value is truthy.
On #2 firstThing.config
is Config
as expected.
So in order to help TypeScript detect the correct type we can use a type predicate as the return type.
interface Config {
foo: string;
bar: string;
}
class Thing {
name: string;
config?: Config;
constructor(name: string, config?: Config) {
this.name = name;
this.config = config;
}
// 👀 notice the return type
hasConfig(): this is { config: Config } {
if (this.config) {
return true;
}
return false;
}
}
let firstThing = new Thing("Apple", { foo:"one", bar: "two" });
if (firstThing.hasConfig()) {
// Now firstThing.config is `Config`
console.log(firstThing.config.foo);
}
Now firstThing.config
is Config
as expected. Have in
mind that even though the type guard says
this is { config: Config }
, TypeScript will merge this type with
the original type definition of Thing
.
Thanks to the nice folks at the TypeScript Discord who helped me solve this issue. Hope this helps more people.