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?
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>;