typescript

Typescript: ReturnType of overloaded function


The type given by ReturnType seems to depend on the order the overload signatures are written

function applyChanges1(input: string): number
function applyChanges1(input: number): string
function applyChanges1(input: number | string): number | string {
  return typeof input === "number" ? input.toString() : input.length
}

function applyChanges2(input: number): string
function applyChanges2(input: string): number
function applyChanges2(input: number | string): number | string {
  return typeof input === "number" ? input.toString() : input.length
}

type Ret1 = ReturnType<typeof applyChanges1> // string
type Ret2 = ReturnType<typeof applyChanges2> // number

It seems to take the return type of the last overload signature which seems quite arbitrary. I was expecting both Ret1 and Ret2 to be string | number. Is there a reason for this behaviour?


Solution

  • As Matt McCutchen points this is a limitation of ReturnType and in general conditional types and multiple overload signatures.

    We can however construct a type that will return all overloaded return types for up to an arbitrary number of overloads:

    function applyChanges1(input: string): number
    function applyChanges1(input: number): string
    function applyChanges1(input: number | string): number | string {
      return typeof input === "number" ? input.toString() : input.length
    }
    
    function applyChanges2(input: number): string
    function applyChanges2(input: string): number
    function applyChanges2(input: number | string): number | string {
      return typeof input === "number" ? input.toString() : input.length
    }
    
    
    type OverloadedReturnType<T> = 
        T extends { (...args: any[]) : infer R; (...args: any[]) : infer R; (...args: any[]) : infer R ; (...args: any[]) : infer R } ? R  :
        T extends { (...args: any[]) : infer R; (...args: any[]) : infer R; (...args: any[]) : infer R } ? R  :
        T extends { (...args: any[]) : infer R; (...args: any[]) : infer R } ? R  :
        T extends (...args: any[]) => infer R ? R : any
    
    
    type RetO1 = OverloadedReturnType<typeof applyChanges1> // string | number 
    type RetO2 = OverloadedReturnType<typeof applyChanges2> // number | string
    

    Playground Link

    The version above will work for up to 4 overload signatures (whatever they may be) but can easily (if not prettily) be extended to more.

    We can even get a union of possible argument types in the same way:

    type OverloadedParameters<T> = 
        T extends { (...args: infer A1) : any; (...args: infer A2) : any; (...args: infer A3) : any ; (...args: infer A4) : any } ? A1|A2|A3|A4  :
        T extends { (...args: infer A1) : any; (...args: infer A2) : any; (...args: infer A3) : any } ? A1|A2|A3 :
        T extends { (...args: infer A1) : any; (...args: infer A2) : any } ? A1|A2  :
        T extends (...args: infer A) => any ? A : any
    
    
    type Params01 = OverloadedParameters<typeof applyChanges1> // [string] | [number]
    type Params02 = OverloadedParameters<typeof applyChanges2>  // [number] | [string]
    

    Playground Link