I'm trying to define an asynchronous type guard. I can do the following synchronously:
class Foo {
public type = 'Foo';
}
// Sync type guard:
function isFoo(obj: any): obj is Foo {
return typeof obj.type !== 'undefined' && obj.type === 'Foo';
}
function useFoo(foo: Foo): void {
alert(`It's a Foo!`);
}
const a: object = new Foo();
if (isFoo(a)) useFoo(a);
But I'm not sure how to do the same async. This is what I tried:
class Bar {
public getType = () => new Promise(resolve => {
setTimeout(() => resolve('Bar'), 1000);
});
}
// Async type guard:
async function isBar(obj: any): Promise<obj is Bar> {
if (typeof obj.getType === 'undefined') return false;
const result = await obj.getType();
return result === 'Bar';
}
function useBar(bar: Bar): void {
alert(`It's a Bar!`);
}
const b: object = new Bar();
isBar(b).then(bIsBar => {
if (bIsBar) useBar(b);
});
Any help would be appreciated!
No, you can't access the guarded parameter from outside the direct scope of the function. So once you return a promise, you can't guard obj
anymore. There is a longstanding open feature request at microsoft/TypeScript#37671 for it.
It might not help though; even if you could express a type guard across scopes, the compiler might widen the type again since there's a chance the value could mutate:
class Bar {
public getType = () => new Promise(resolve => {
setTimeout(() => resolve('Bar'), 1000);
});
public barProp: string; // added to distinguish structurally from NotBar
}
class NotBar {
public getType = () => new Promise(resolve => {
setTimeout(() => resolve('NotBar'), 1000);
});
public notBarProp: string; // added to distinguish structurally from Bar
}
function useBar(bar: Bar): void {
alert(`It's a Bar!`);
}
function useNotBar(notBar: NotBar): void {
alert(`Nope, not a Bar.`)
}
var b: Bar | NotBar = new Bar();
if (b instanceof Bar) {
useBar(b); // narrowed to Bar, no error
isBar(b).then(bIsBar => {
useBar(b); // error! widened to Bar | NotBar again
})
}
As a possible workaround, you can invent your own "type guard" object and pass that back, although it's not as pleasant to use:
type Guarded<Y, N = any> = { matches: true, value: Y } | { matches: false, value: N };
function guarded<Y, N = any>(v: Y | N, matches: boolean): Guarded<Y, N> {
return matches ? { matches: true, value: <Y>v } : { matches: false, value: <N>v };
}
// Async type guard:
async function isBar<N extends { getType?: () => Promise<any> } = any>(obj: Bar | N): Promise<Guarded<Bar, N>> {
if (typeof obj.getType === 'undefined') return guarded(obj, false);
const result = await obj.getType();
return guarded(obj, result === 'Bar');
}
isBar(b).then(bIsBar => {
if (bIsBar.matches) useBar(bIsBar.value);
});
isBar<NotBar>(b).then(bIsBar => {
if (bIsBar.matches) useBar(bIsBar.value); else useNotBar(bIsBar.value);
});