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