Search code examples
typescripttypeguards

How to define TypeScript return type in a method to infer variable existence based on a parameterized type?


I have a TypeScript class called Design which contains a method named checkFetched. This method is designed to check if a property of type DesignData exists within the class instance based on a parameterized type called Filename. Here's a simplified version of the code:

type Filename = 'original' | 'result'
type DesignData = {
  id: string
  // ...
}

export class Design {
  original?: DesignData
  result?: DesignData

  checkFetched(target: Filename): this is { [K in typeof target]: DesignData } {
    return !!this[target]
  }

  private async loadDesign(fileName: Filename) {
    if (!this.checkFetched(fileName)) return
    const shouldNotHaveUndefined = this[fileName]
    const shouldHaveUndefined1 = this.original
    const shouldHaveUndefined2 = this.result
  }

  private async loadDesign2() {
    if (!this.checkFetched('original')) return
    const shouldNotHaveUndefined = this.original
    const shouldHaveUndefined = this.result
  }
}

The checkFetched method returns a boolean value indicating whether the specified property exists or not. However, I want to refine the return type of this method in a way that if the check passes, TypeScript should infer that the corresponding property is not undefined, while other properties could still be undefined.

For example, after calling checkFetched('original') in the loadDesign2 method, TypeScript should understand that shouldNotHaveUndefined is not undefined, but shouldHaveUndefined1 and shouldHaveUndefined2 could be undefined. For now, shouldHaveUndefined will be treated as not undefined.

Given my attempts, I expected that by refining the return type, TypeScript would infer the existence of the property correctly after calling checkFetched.

I'm seeking insights from the community to determine if this approach can be modified to meet my needs or if this requirement is inherently impossible to achieve in TypeScript.

Please use this TypeScript Playground link to explore and share your insights: TypeScript Playground


Solution

  • Your current checkFetched result type is too broad, it says that any property whose name is in the Filename union (that is, both original and result) is not undefined, but you want it to tell TypeScript that the property with the specific name in target is not undefined. To do that, you make the function generic:

    checkFetched<Target extends Filename>(target: Target): this is { [K in Target]: DesignData } {
        return !!this[target];
    }
    

    (this is { [K in Target]: DesignData } could also be written this is Record<Target, DesignData>, which by using an idiom may be a bit clearer — or not, it's a style choice.)

    Then the checks work the way I think you mean:

    private async loadDesign<Target extends Filename>(fileName: Target) {
        if (!this.checkFetched(fileName)) return;
        const shouldNotHaveUndefined = this[fileName];
        //    ^? const shouldNotHaveUndefined: (this & { [K in Target]: DesignData; })[Target]
        console.log(shouldNotHaveUndefined.id); // (Just to show that the above really means it's not `undefined`)
        // No error −−−−−−−−−−−−−−−−−−−−−−−^
        const shouldHaveUndefined1 = this.original;
        //    ^? const shouldHaveUndefined1: DesignData | undefined
        const shouldHaveUndefined2 = this.result;
        //    ^? const shouldHaveUndefined2: DesignData | undefined
    }
    
    private async loadDesign2() {
        if (!this.checkFetched("original")) return;
        const shouldNotHaveUndefined = this.original;
        //    ^? const shouldNotHaveUndefined: DesignData
        const shouldHaveUndefined = this.result;
        //    ^? const shouldHaveUndefined: DesignData | undefined
    }
    

    Updated playground