Search code examples
typescriptasynchronoustypes

TypeScript async type guard using Promise


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);
});

Try it here

Any help would be appreciated!


Solution

  • 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);
    });