TypeScript – Type ‘boolean’ does not satisfy the constraint of a type that returns a boolean

I have a TypeScript class that manages a cache of objects. It has a get method that takes an item’s ID, and an optional boolean for whether or not an API should be called to fetch the object if it isn’t already cached. If the boolean parameter is false or undefined, the method should return CachedObject | undefined. Otherwise, it should return Promise<CachedObject | undefined>. However, the boolean parameter should only be usable if the Cache class’s FetchObject type parameter is true:

class ExampleClass {};
type CachedObject = ExampleClass;

type GetFetch<FetchObject> = FetchObject extends true ? boolean : false;

type Result<CachedObject, Fetch> = Fetch extends true ? Promise<CachedObject | undefined> : (CachedObject | undefined);

export default class Cache<FetchObject = true> {

    get<Fetch extends GetFetch<FetchObject> = false>(id: string, fetch?: Fetch): Result<CachedObject, Fetch> {

        //
        return;
    }
}

new Cache<false>().get("123"); // ExampleClass | undefined
new Cache().get("123"); // ExampleClass | undefined
new Cache().get("123", true); // Promise<ExampleClass | undefined>

However, line 10 (get<...>(...)) has the following error for the = false default type parameter:

Type 'boolean' does not satisfy the constraint 'GetFetch<FetchObject>'

I found that if FetchObject is explicitly defined without a class:

type FetchObject = true;

export default function get<Fetch extends GetFetch<FetchObject> = false>(id: string, fetch?: Fetch): Result<CachedObject, Fetch> {

    //
    return;
}

There aren’t any errors and the get function can be used just fine. How come this doesn’t work as a type parameter for a class?

Answer

This one is tricky. I try my best to explain what happens. Typescript doesn’t fully evaluate generics until it has to (for performance reasons). GetFetch<FetchObject> only consists of generics that could be anything, so the compiler doesn’t evaluate yet. This would be different if you already put in some real types like you did with type FetchObject = true;. The problem now arises when you want to set the default value of Fetch to false. Because GetFetch<FetchObject> isn’t fully evaluated yet, the compiler doesn’t know that the expression FetchObject extends true ? boolean : false; can only be boolean. And whatever the compiler thinks GetFetch<FetchObject> would evaluate to, we know for sure it’s not boolean. The solution to this problem is to either force the compiler to believe the expression can only be boolean or to move the expression to a point where it has to be fully evaluated. The best way I see to solve this problem is to move the evaluation to the function result like this:

class ExampleClass {};
type CachedObject = ExampleClass;

type Result<CachedObject, Fetch> = Fetch extends true ? Promise<CachedObject | undefined> : (CachedObject | undefined);

// prevent fetching, FetchObject= true
type CanBeFetched<FetchedObject, Fetch> = FetchedObject extends true ? Fetch : never;

export default class Cache<FetchObject = true> {

    get<Fetch extends boolean = false>(id: string, fetch?: CanBeFetched<FetchObject, Fetch>): Result<CachedObject, Fetch> {
        // do something
        return null as any;
    }
}

new Cache<false>().get("123"); // ExampleClass | undefined
new Cache<false>().get("123", true); // Error, can't be used when FetchObject = false.
new Cache().get("123"); // ExampleClass | undefined
new Cache().get("123", true); // Promise<ExampleClass | undefined>
new Cache().get("123", false); // ExampleClass | undefined

Leave a Reply

Your email address will not be published. Required fields are marked *