Search code examples
typescripttypescript-genericsreturn-typemapped-typesnested-generics

Apply ReturnType on union type


Is TypeScript incapable of using ReturnType on union types?

type NumberParser = (input: string) => number | DiplomacyError;
type StringParser = (input: string) => string | DiplomacyError;
type Parser = NumberParser | StringParser;

export interface Schema {
  [key: string]: Parser | Schema;
}

export type RawType<T extends Schema> = {
  [Property in keyof T]: T[Property] extends Schema
    ? RawType<T[Property]>
    : ReturnType<T[Property]>; // T[Property] marked as error
};

<T[Property]> gives the following error:

Type 'T[Property]' does not satisfy the constraint '(...args: any) => any'.
  Type 'T[keyof T]' is not assignable to type '(...args: any) => any'.
    Type 'T[string] | T[number] | T[symbol]' is not assignable to type '(...args: any) => any'.
      Type 'T[string]' is not assignable to type '(...args: any) => any'.
        Type 'Parser | Schema' is not assignable to type '(...args: any) => any'.
          Type 'Schema' is not assignable to type '(...args: any) => any'.
            Type 'Schema' provides no match for the signature '(...args: any): any'.ts(2344)

Solution

  • It is a known issue in TypeScript that the false branch of a conditional type does not get its types narrowed. So in T extends U ? F<T> : G<T> does not take G<T> and replace it with something like G<Exclude<T, U>>. As far as the compiler is concerned, the T in G<T> might still be assignable to U, even though it's obvious to us that it won't be. See microsoft/TypeScript#29188. It looks like there was some work done to address this at microsoft/TypeScript#24821, but it was not merged. It's not clear to me if or when this issue will be resolved.

    Until then, it's easy enough (if annoying) to do such narrowing yourself when necessary:

    export type RawType<T extends Schema> = {
        [K in keyof T]: T[K] extends Schema
        ? RawType<T[K]>
        : ReturnType<Exclude<T[K], Schema>>;
    };
    

    Or possibly

    export type RawType<T extends Schema> = {
        [K in keyof T]: T[K] extends Schema
        ? RawType<T[K]>
        : ReturnType<Extract<T[K], Parser>>;
    };
    

    Playground link to code