Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: ReturnType of overloaded function

Tags:

typescript

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?

like image 367
Lionel Tay Avatar asked Oct 11 '18 12:10

Lionel Tay


People also ask

Is function overloading allowed in TypeScript?

TypeScript provides the concept of function overloading. You can have multiple functions with the same name but different parameter types and return type. However, the number of parameters should be the same.

What are the restrictions of overloaded functions?

Restrictions on overloadingAny two functions in a set of overloaded functions must have different argument lists. Overloading functions that have argument lists of the same types, based on return type alone, is an error.

What is the syntax of an overloaded function?

Syntax. As explained from the definition of Function Overloading in C++, to overload two or more functions, they must have the same function name and different parameters that are either a different number of parameters or different data types of parameters or both.


2 Answers

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]
like image 179
Titian Cernicova-Dragomir Avatar answered Sep 28 '22 04:09

Titian Cernicova-Dragomir


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

e.g.:

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

like image 37
Aidin Avatar answered Sep 28 '22 03:09

Aidin