简体   繁体   中英

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 string | number . Is there a reason for this behaviour?

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

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 OverloadedArguments<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 RetO1 = OverloadedArguments<typeof applyChanges1> // [string] & [number]
type RetO2 = OverloadedArguments<typeof applyChanges2>  // [number] & [string]

I had a similar problem -- I needed to pick the ReturnType of the exact overload, based on the arguments I have.

eg:

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


// Needed:

type Ret11 = ReturnTypeWithArgs<typeof applyChanges1, [string]> // number
type Ret12 = ReturnTypeWithArgs<typeof applyChanges1, [number]> // string
type Ret13 = ReturnTypeWithArgs<typeof applyChanges1, [boolean]> // object
type Ret14 = ReturnTypeWithArgs<typeof applyChanges1, [number | string]> // number | string
type Ret15 = ReturnTypeWithArgs<typeof applyChanges1, [number | boolean]> // string | object
type Ret16 = ReturnTypeWithArgs<typeof applyChanges1, [number | string | boolean]> // number | string | object

So I created the following ReturnTypeWithArgs utility based on the beautiful answer of @Titian above and using Extract .

type ReturnTypeWithArgs<T extends (...args: any[]) => any, ARGS_T> =
    Extract<
        T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; } ? [A1, R1] | [A2, R2] | [A3, R3] | [A4, R4] :
        T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; } ? [A1, R1] | [A2, R2] | [A3, R3] :
        T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; } ? [A1, R1] | [A2, R2] :
        T extends { (...args: infer A1): infer R1; } ? [A1, R1] :
        never,
        [ARGS_T, any]
    >[1]

It works like a charm!

Playground Link


PS. In my real case I have 7 overloads, so wish me luck! ;D

This is a known limitation . The TypeScript team's recommendation is to include a "most general" overload signature as your last overload signature, eg:

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

Titian Cernicova-Dragomir has a nicer alternate solution in his answer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM