Non-strict (i.e. sometimes eager, sometimes lazy) type checking would really help type the more functional and dynamic parts of JavaScript, such as Function.prototype.bind (it's impossible to type that properly without non-strict checking of types) and currying. And to be honest, it gets highly repetitive using the current hack of interface + type alias for recursive types like Nested<T>. [1]
// NowinterfaceNestedArray<T>extendsArray<Nested<T>>{}typeNested<T>=T|NestedArray<T>// Non-strict type checkingtypeNested<T>=T|Nested<T>[]Here's an example of curry, with the interface hack and with non-strict type checking (this also requires rest types, which might also require non-strict resolution of them):
// With interfaces...I'm not sure CurryN/CurryN1 will even check here.typeCurry0<R,T>=(this: T)=>RinterfaceCurry1<R,T,A>{(): Curry1<R,T,A>((this: T,arg: A)): R}interfaceCurryN<R,T,B, ...A>extendsCurryN1<R,T, ...A>{(): CurryN<R,T,B, ...A>(this: T, ...args: [...A,B]): R(...arg: [...A]): CurryN<R,T,B>}interfaceCurryN1<R,T, ...A>extendsCurryN<R,T, ...A>{}functioncurry<R,T>(f: (this: T)=>R): Curry0<R,T>functioncurry<R,T,A>(f: (this: T,arg: A)=>R): Curry1<R,T,A>functioncurry<R,T,B, ...A>(f: (this: T, ...args: [...A])=>R): CurryN<R,T,B, ...A>// Non-strict type checkingtypeCurry0<R,T>=(this: T)=>RtypeCurry1<R,T,A>=(()=>Curry1<R,T,A>)|((this: T,arg: A)=>R)typeCurryN<R,T,B, ...A>=(()=>CurryN<R,T,B, ...A>)|((this: T, ...args: [...A,B])=>R)|((...arg: [...A])=>CurryN<R,T,B>)|CurryN<R,T, ...A>functioncurry<R,T>(f: (this: T)=>R): Curry0<R,T>functioncurry<R,T,A>(f: (this: T,arg: A)=>R): Curry1<R,T,A>functioncurry<R,T,B, ...A>(f: (this: T, ...args: [...A])=>R): CurryN<R,T,B, ...A>Also, to explain bind, it will likely need variadic types and a way to split a variadic type as well, but that'll require non-strict type checking to correctly infer the type to even check.
interfaceFunction<R,T, ...A>{bind[...A= ...X, ...Y](this: (this: T, ...args: [...X, ...Y])=>R,thisObject: T,
...args: [...X]): (this: any, ...rest: [...Y])=>R}// Valuesdeclarefunctionfunc(a: number,b: string,c: boolean,d?: symbol): numberletf=func.bind(null,1,"foo")// How to inferbind[...A= ...X, ...Y]<R,T>(this: (this: T, ...args: [...X, ...Y])=>R,thisObject: T,
...args: [...X]): (this: any, ...rest: [...Y])=>R// Infer first type parameterbind[...A= ...X, ...Y]<number,T>(this: (this: T, ...args: [...X, ...Y])=>number,thisObject: T,
...args: [...X]): (this: any, ...rest: [...Y])=>number// Infer second type parameterbind[...A= ...X, ...Y]<number,any>(this: (this: any, ...args: [...X, ...Y])=>number,thisObject: any,
...args: [...X]): (this: any, ...rest: [...Y])=>number// Infer first part of rest parameterbind[...A=number, ...*X, ...Y]<number, any>(this: (this: any, ...args: [number, ...*X, ...Y])=> number,thisObject: any,
...args: [number, ...*X]): (this: any, ...rest: [...Y])=> number
// Infer second part of rest parameterbind[...A= number, string, ...*X, ...Y]<number, any>(this: (this: any, ...args: [number, string, ...*X, ...Y])=> number,thisObject: any,
...args: [number, string, ...*X]): (this: any, ...rest: [...Y])=> number
// First rest parameter ends: all ones that only uses it are fully spreadbind[...A= number, string, ...Y]<number, any>(this: (this: any, ...args: [number, string, ...Y])=> number,thisObject: any,
...args: [number, string]): (this: any, ...rest: [...Y])=> number
// Infer first part of next rest parameterbind[...A= number, string, boolean, ...*Y]<number, any>(this: (this: any, ...args: [number, string, boolean, ...*Y])=> number,thisObject: any,
...args: [number, string]): (this: any, ...rest: [boolean, ...*Y])=> number
// Infer second part of next rest parameter// Note that information about optional parameters are retained.bind[...A= number, string, boolean, symbol?, ...*Y]<number, any>(this: (this: any,
...args: [number, string, boolean, symbol?, ...*Y])=> number,thisObject: any,
...args: [number, string]): (this: any, ...rest: [boolean, symbol?, ...*Y])=> number
// Second rest parameter ends: all ones that only uses it are exhaustedbind[...A= number, string, boolean, symbol?]<number, any>(this: (this: any, ...args: [number, string, boolean, symbol?])=> number,thisObject: any,
...args: [number, string]): (this: any, ...rest: [boolean, symbol?])=> number
// All rest parameters that are tuples get converted to multiple regularparametersbind[...A= number, string, boolean, symbol?]<number, any>(this: (this: any,x0: number,x1: string,x2: boolean,x3?: symbol
)=> number,thisObject: any,x0: number,x1: string
): (this: any,x0: boolean,x1?: symbol)=> number
// And this checksI'm also thinking this might reduce the number of explicit generics as well, since it does a mixture of breadth-first and depth-first, stopping when it can't infer something yet. Also, when verifying generic types themselves, it would collect metadata to verify that yes, it could theoretically check.
Yes, I know this is probably pretty difficult [2], and it does theoretically make the template system Turing-complete beyond the compiler's ability to stop that, but in practice, you can set a nesting limit to something like 10,000 or similar, which should be more than plenty for most cases. C++ compilers already do the same themselves, since Turing machines have already been implemented in C++ templates, taking advantage of variadic template arguments alone.
[1] This originally was realized and conceived in this bug and elaborated in this comment.
[2] It's literally enabling a pair of variadic types to be spread across tuples and rest parameters, which is rank-2n polymorphism. This is why bind is a beast to type.
Non-strict (i.e. sometimes eager, sometimes lazy) type checking would really help type the more functional and dynamic parts of JavaScript, such as Function.prototype.bind (it's impossible to type that properly without non-strict checking of types) and currying. And to be honest, it gets highly repetitive using the current hack of interface + type alias for recursive types like
Nested<T>. [1]Here's an example of
curry, with the interface hack and with non-strict type checking (this also requires rest types, which might also require non-strict resolution of them):Also, to explain
bind, it will likely need variadic types and a way to split a variadic type as well, but that'll require non-strict type checking to correctly infer the type to even check.I'm also thinking this might reduce the number of explicit generics as well, since it does a mixture of breadth-first and depth-first, stopping when it can't infer something yet. Also, when verifying generic types themselves, it would collect metadata to verify that yes, it could theoretically check.
Yes, I know this is probably pretty difficult [2], and it does theoretically make the template system Turing-complete beyond the compiler's ability to stop that, but in practice, you can set a nesting limit to something like 10,000 or similar, which should be more than plenty for most cases. C++ compilers already do the same themselves, since Turing machines have already been implemented in C++ templates, taking advantage of variadic template arguments alone.
[1] This originally was realized and conceived in this bug and elaborated in this comment.
[2] It's literally enabling a pair of variadic types to be spread across tuples and rest parameters, which is rank-2n polymorphism. This is why
bindis a beast to type.