Search code examples
typescripttypestypescript-types

Is it possible to overload Generic parameters in TypeScript?


I'm coming from C# and there you have functions parameters defined as a class.

Action        // void method
Action<T1>    // void method with a T1 parameter 
Action<T1,T2> // void method with a T1 and T2 parameter 
Func<T1>      // method that returns T1
Func<T1,T2>   // method with a T1 parameter, returning T2

etc

I understand this is opiniated but for me:

function Koala(func1:Action, func2:Func<string,number>)

is more readable then:

function Koala(func1:()=>void, func1:(value1:string)=>number)

on the backend we already use the first one. So our brains are already hardwired to it and it's easier to scan.

So the question is - is it possible to create something similar in TypeScript?

type Action = ()=>void;
type Action1<T1> = (a:T1)=>void;
type Action2<T1,T2> = (a:T1,b:T2)=>void;
type Action3<T1,T2,T3> = (a:T1,b:T2,c:T3)=>void;

type Func<T1> = ()=>T1;
type Func1<T1,T2> = (a:T1)=>T2;
type Func2<T1,T2,T3> = (a:T1,b:T2)=>T3;

var cheese:Func1<string,boolean>

let value = cheese('test');

This works of course. But I prefer to have the types overloaded instead of the 1,2,3 suffix, so Action<string,bool> automatically picks the right overload.

Is that possible?


Solution

  • TypeScript does not have overloaded generic types. There is an open feature request at microsoft/TypeScript#39526 but so far they are not part of the language.

    You can mostly simulate them with default type arguments using a default that is unlikely for people to manually specify. For example:

    type Func<T1, T2 = Blank, T3 = Blank> =
      [T3] extends [Blank] ? (
        [T2] extends [Blank] ? () => T1 : (a: T1) => T2
      ) : (a: T1, b: T2) => T3
    

    where you define Blank to be the never type or possibly some nonce type like a unique symbol:

    declare const __blank: unique symbol;
    type Blank = typeof __blank
    

    Essentially we use conditional types to implement "overload resolution". If T2 and T3 are both Blank then you use the one-arg type. If just T3 is Blank then you use the two-arg type. And if T3 isn't Blank then you use the three-arg type.

    This behaves as you desired:

    declare var cheese: Func<string, boolean>
    // var cheese: (a: string) => boolean
    let value = cheese('test');
    

    I wouldn't really recommend trying this, though, unless your use case is so overwhelmingly compelling that it's worth adding complexity and possible fragility to your types.

    For the particular examples shown, you can just use rest tuples to represent functions of different arity in a single type:

    type Func<A extends any[], R> = (...args: A) => R;
    declare var cheese: Func<[string], boolean>
    let value = cheese('test');
    
    declare var bacon: Func<[string, number, Date, boolean], void>;
    // var bacon: (args_0: string, args_1: number, args_2: Date, args_3: boolean) => void
    

    Doing it this way works with the type system instead of against it.

    The first version, fully written out (but not-recommended) is:

    type Action0                = ()=>void;
    type Action1<T1>            = (a:T1)=>void;
    type Action2<T1,T2>         = (a:T1,b:T2)=>void;
    type Action3<T1,T2,T3>      = (a:T1,b:T2,c:T3)=>void;
    type Action4<T1,T2,T3,T4>   = (a:T1,b:T2,c:T3,d:T4)=>void;
    
    type Func1<T1>          = ()=>T1;
    type Func2<T1,T2>       = (a:T1)=>T2;
    type Func3<T1,T2,T3>    = (a:T1,b:T2)=>T3;
    type Func4<T1,T2,T3,T4> = (a:T1,b:T2,c:T3)=>T4;
    
    declare type Action<T1=never,T2=never,T3=never,T4=never> = 
        [T1] extends [never] ?  Action0 : 
        [T2] extends [never] ?  Action1<T1> : 
        [T3] extends [never] ?  Action2<T1,T2> :
        [T4] extends [never] ?  Action3<T1,T2,T3> :
                                Action4<T1,T2,T3,T4>;
                            
    declare type IFunc<T1,T2=never,T3=never,T4=never> = 
        [T2] extends [never] ?  Func1<T1> : 
        [T3] extends [never] ?  Func2<T1,T2> :
        [T4] extends [never] ?  Func3<T1,T2,T3> :
                                Func4<T1,T2,T3,T4>;
    

    Playground link