Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-strict type checking #7489

Closed
dead-claudia opened this issue Mar 12, 2016 · 5 comments
Closed

Non-strict type checking #7489

dead-claudia opened this issue Mar 12, 2016 · 5 comments
Labels
Duplicate An existing issue was already created

Comments

@dead-claudia
Copy link

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.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 28, 2016

I am not sure i see a concrete proposal here. this all depends on #5453 anyways.

@mhegazy mhegazy closed this as completed Mar 28, 2016
@mhegazy mhegazy added the Duplicate An existing issue was already created label Mar 28, 2016
@dead-claudia
Copy link
Author

@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.

@dead-claudia
Copy link
Author

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.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 30, 2016

ah. thanks for the explanation. here is the issue tracking this: #6230, there is a discussion about why it is hard to implement given the current type system in #3496 as well.

@dead-claudia
Copy link
Author

I think it's good to consider this a dupe of that, then.

dokidokivisual added a commit to karen-irc/karen that referenced this issue May 4, 2016
chore(TypeScript): Enable 'strictNullChecks' option

This tries to enable [`--strictNullChecks` option](microsoft/TypeScript#7140) of TypeScript compiler.

- [Non-nullable types by ahejlsberg · Pull Request #7140 · Microsoft/TypeScript](microsoft/TypeScript#7140)
  - [Non-strict type checking · Issue #7489 · Microsoft/TypeScript](microsoft/TypeScript#7489)
  - [[Request for feedback] Nullable types, `null` and `undefined` · Issue #7426 · Microsoft/TypeScript](microsoft/TypeScript#7426)
- [Control flow based type analysis by ahejlsberg · Pull Request #8010 · Microsoft/TypeScript](microsoft/TypeScript#8010)

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="35" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/karen-irc/karen/604)
<!-- Reviewable:end -->
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

2 participants