Search code examples
typescript

Error when returning conditional type from a generic method


I'm encountering a TypeScript error while implementing a conditional type in a MessagesStore object. Here's the complete code setup:

type Message = { id: number; text: string };

type MessagesStore = {
    queue: Array<Message>;
    read: <T extends boolean>(assertiveMode?: T) => T extends true ? Message : Message | undefined;
}

const store: MessagesStore = {
    queue: [],

    read(assertiveMode: boolean = false) {
        const message = this.queue.shift();

        if (assertiveMode && message === undefined) {
            throw new Error('The messages queue is empty');
        }

        return message;
    }
}

However, TypeScript throws the following error:

Type '<T extends boolean>(assertiveMode?: boolean) => Message | undefined' is not assignable to type '<T extends boolean>(assertiveMode?: T | undefined) => T extends true ? Message : Message | undefined'.
  Type 'Message | undefined' is not assignable to type 'T extends true ? Message : Message | undefined'.
    Type 'undefined' is not assignable to type 'T extends true ? Message : Message | undefined'.

I'm struggling to understand why I'm getting this error and how to resolve it.


Solution

  • Currently, as of TypeScript 5.3, there is no compiler-verified type-safe way to return a conditional type that depends on a generic type parameter. There is a longstanding open feature request at microsoft/TypeScript#33912 to support this, but for now it's not part of the language.

    The problem is currently that when you check assertiveMode, it can narrow the type of assertiveMode from T to true or false, but it doesn't have any effect on the generic type parameter T itself. That stays unchanged, and so the compiler still cannot tell if message is a suitable value of type T extends true ? Message : Message | undefined.

    It's possible that sometime soon this will be addressed; there is a pull request at microsoft/TypeScript#56941 which might possibly get merged at some point, and I see that dealing with these sort of generic functions is on the road map for TypeScript 5.5 at microsoft/TypeScript#57475. So maybe your code will magically start working with no errors.

    Until and unless that happens, you'll need to loosen the type checking to get your code to compile. The easiest way to do that is by using the any type to say that we don't want to try to check the return type at all:

    const store: MessagesStore = {
      queue: [],
    
      read(assertiveMode: boolean = false) {
        const message = this.queue.shift();
    
        if (assertiveMode && message === undefined) {
          throw new Error('The messages queue is empty');
        }
    
        return message as any; // 🤷‍♀️ ms/TS#33912
      }
    }
    

    It's unfortunate, but for right now, generic conditional return types are much more useful for function callers and not function implementers.

    Playground link to code