Non-strict type checking
dead-claudia opened this issue · 5 comments
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 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.
@mhegazy Not exactly. What I'm proposing is having type matching not be eager. This is an example of what I'd like to see work, but currently errors out because it's a directly recursive type.
type Nested<T> = T | Array<Nested<T>>;The current workaround is a little hackish, and not very pretty:
interface NestedArray<T> extends Array<Nested<T>> {}
type Nested<T> = T | NestedArray<T>;What this would match is structures like this (it's useful for virtual DOM structures, etc.):
// Using Mithril as an example
const children: Nested<VirtualNode<any>> = [
m("h2", "Task List"),
tasks.map(task => m(Task, task)),
]There's other places where such recursion would help. Promise.resolve and friends also recursively flatten thenables.
Although it's related to #5453 with specifically partial application (Function.prototype.bind, _.curry, etc.), I feel it's independent enough to be discussed separately on its own merits.
I don't really know enough about TypeScript's type system to make a more concrete proposal than this, though. This change would be technically non-breaking, but I expect it would be very difficult.
I think it's good to consider this a dupe of that, then.