Description
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]
// Now
interface NestedArray<T> extends Array<Nested<T>> {}
type Nested<T> = T | NestedArray<T>
// Non-strict type checking
type Nested<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.
type Curry0<R, T> = (this: T) => R
interface Curry1<R, T, A> {
(): Curry1<R, T, A>
((this: T, arg: A)): R
}
interface CurryN<R, T, B, ...A> extends CurryN1<R, T, ...A> {
(): CurryN<R, T, B, ...A>
(this: T, ...args: [...A, B]): R
(...arg: [...A]): CurryN<R, T, B>
}
interface CurryN1<R, T, ...A> extends CurryN<R, T, ...A> {}
function curry<R, T>(f: (this: T) => R): Curry0<R, T>
function curry<R, T, A>(f: (this: T, arg: A) => R): Curry1<R, T, A>
function curry<R, T, B, ...A>(f: (this: T, ...args: [...A]) => R): CurryN<R, T, B, ...A>
// Non-strict type checking
type Curry0<R, T> = (this: T) => R
type Curry1<R, T, A> = (() => Curry1<R, T, A>) | ((this: T, arg: A) => R)
type CurryN<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>
function curry<R, T>(f: (this: T) => R): Curry0<R, T>
function curry<R, T, A>(f: (this: T, arg: A) => R): Curry1<R, T, A>
function curry<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.
interface Function<R, T, ...A> {
bind[...A = ...X, ...Y](
this: (this: T, ...args: [...X, ...Y]) => R,
thisObject: T,
...args: [...X]
): (this: any, ...rest: [...Y]) => R
}
// Values
declare function func(a: number, b: string, c: boolean, d?: symbol): number
let f = func.bind(null, 1, "foo")
// How to infer
bind[...A = ...X, ...Y]<R, T>(
this: (this: T, ...args: [...X, ...Y]) => R,
thisObject: T,
...args: [...X]
): (this: any, ...rest: [...Y]) => R
// Infer first type parameter
bind[...A = ...X, ...Y]<number, T>(
this: (this: T, ...args: [...X, ...Y]) => number,
thisObject: T,
...args: [...X]
): (this: any, ...rest: [...Y]) => number
// Infer second type parameter
bind[...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 parameter
bind[...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 parameter
bind[...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 spread
bind[...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 parameter
bind[...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 exhausted
bind[...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 regular
parameters
bind[...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 checks
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 bind
is a beast to type.