Skip to content

Type narrowing strategy for most closest type selection #6589

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

Closed
falsandtru opened this issue Jan 23, 2016 · 17 comments
Closed

Type narrowing strategy for most closest type selection #6589

falsandtru opened this issue Jan 23, 2016 · 17 comments
Labels
Duplicate An existing issue was already created

Comments

@falsandtru
Copy link
Contributor

class A<T> {
    private x: number;
    public m(p: T): T {
        return p;
    }
}
class B extends A<string> {
    private y: number;
}
var a: A<number> | B;
if (a instanceof B) {
    a.m(''); // `a` is `B`
}
if (a instanceof A) {
    a.m(1); // `a` is `A<number> | B`, should be `A<number>`
}
@DanielRosenwasser
Copy link
Member

Why would you want to narrow to A<number> here? B extends from A, so even if a had the type B at runtime, a instanceof B would turn out to be true.

@DanielRosenwasser DanielRosenwasser added the By Design Deprecated - use "Working as Intended" or "Design Limitation" instead label Jan 24, 2016
@falsandtru
Copy link
Contributor Author

When I remove the type parameter, type narrowing succeeds in that case.

class A {
    private x: number;
    public m(p: number): number {
        return p;
    }
}
class B extends A {
    private y: number;
}
var a: A | B;
if (a instanceof A) {
    a.m(1); // `a` is `A`, not `A | B`
}

Therefore, this narrowing failure caused by type parameters. Why does narrowing fail just when I use type parameters? It seems a bug.

@DanielRosenwasser
Copy link
Member

I think that's just because we perform subtype reduction within that branch, so you lose B. I'm not sure if we should be doing that. Thoughts @ahejlsberg and @weswigham?

@DanielRosenwasser
Copy link
Member

Though, in some sense, I don't entirely see why it's wrong to do that. Is there anything you can't do because we performed subtype reduction?

@falsandtru
Copy link
Contributor Author

This is my actual case.

// monad.ts
export abstract class Monad<T> {
  private MONAD: T;
  constructor(protected thunk?: () => Monad<T>) {
  }
  public extract(): T
  public extract<U>(transform: (val: any) => U): T | U
  public extract<U>(transform?: (val: any) => U): T | U {
    return this.memory_
      ? this.memory_.extract(transform)
      : this.evaluate().extract(transform);
  }
  protected memory_: this;
  protected evaluate(): this {
    return this.memory_ = this.memory_ || <this>this.thunk();
  }
  public constrain<S extends Monad<T>>(type?: S): Monad<T> {
    return this;
  }
}
// maybe.ts
import {Monad} from './monad';

export class Maybe<T> extends Monad<T> {
  private MAYBE: T | void;
  constructor(protected thunk?: () => Maybe<T>) {
    super(thunk);
  }
  public bind(f: (val: T) => Maybe<T>): Maybe<T>
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U>
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U> {
    return new Maybe<U>(() => {
      const m: Just<T> | Nothing | Maybe<T> = this.evaluate();
      if (m instanceof Just) {
        return f(m.extract());
      }
      if (m instanceof Nothing) {
        return m;
      }
      if (m instanceof Maybe) {
        return (<Maybe<T>>m).bind<U>(f);
      }
      throw new TypeError(`ArchStream: Maybe: Invalid monad value.\n\t${m}`);
    });
  }
  public extract(): T
  public extract<U>(transform: () => U): T | U
  public extract<U>(transform?: () => U): T | U {
    return super.extract(transform);
  }
  public constrain<S extends Just<T>>(type?: S): void
  public constrain<S extends Nothing>(type?: S): void
  public constrain<S extends Maybe<T>>(type?: S): Maybe<T>
  public constrain<S>(type?: S): Maybe<T> {
    return this;
  }
}

export class Just<T> extends Maybe<T> {
  private TYPE: Just<T>;
  constructor(private val_: T) {
    super();
  }
  public bind(f: (val: T) => Just<T>): Just<T>
  public bind(f: (val: T) => Nothing): Nothing
  public bind<U>(f: (val: T) => Just<U>): Just<U>
  public bind(f: (val: T) => Maybe<T>): Maybe<T>
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U>
  public bind<U>(f: (val: T) => Maybe<U>): any {
    return new Maybe(() => this).bind(f);
  }
  public extract<U>(transform?: () => U): T {
    return this.val_;
  }
  public constrain<S extends Just<T> | Maybe<T>>(type?: S): Just<T> {
    return this;
  }
}

export class Nothing extends Maybe<any> {
  private TYPE: Nothing;
  public bind(f: (val: any) => Maybe<any>): Nothing {
    return this;
  }
  public extract(): any
  public extract<U>(transform: () => U): U
  public extract<U>(transform?: () => U): U {
    if (!transform) throw void 0;
    return transform();
  }
  public constrain<S extends Nothing | Maybe<any>>(type?: S): Nothing {
    return this;
  }
}

expected:

      if (m instanceof Maybe) {
        return m.bind<U>(f); // `m` is `Maybe<T>`
      }

actual:

      if (m instanceof Maybe) {
        return (<Maybe<T>>m).bind<U>(f); // `m` is `Nothing | Maybe<T>`, not event `Just<T> | Nothing | Maybe<T>`
      }

@DanielRosenwasser
Copy link
Member

It looks you do want subtype reduction and you aren't getting it. The reason is extremely subtle (and it took a non-trivial amount of time to exploit it). Here's a smaller repro:

class A<T> {
    prop: T;
}

class B<T> extends A<T> {
}

class C extends A<any> {
    cProp: any;
}

var x: A<string> | B<string> | C;
if (x instanceof A) {
    x; // x has type 'A<string> | C'
}

First we'll narrow your types against A<any>, and since A<any> is a subtype of A<string>, B<string>, and C, they'll all stick around.

Then we'll try to perform subtype reduction. Since B<string> is a subtype of A<string>, we'll get rid of B<string>. So far so good.

What about C? Is C a subtype of A<string>? Surprisingly, it's not!

The issue is that you derived from A<any> which means that C has the property prop: any. Meanwhile, A<string>'s prop has the type string. While any is assignable to string, it's not a subtype. That means that C can't be reduced to a A<string> during subtype reduction.

We'll also try to check if A<string> is a subtype of C, which it's not since A<string> has a cProp.

You'll end up with x narrowed down to A<string> | C.

I'm going to close this issue so that we can tackle the real problem that you ran into.

Actually I'll leave it open. No reason we can't continue discussion here.

@DanielRosenwasser
Copy link
Member

@ahejlsberg @RyanCavanaugh @weswigham any thoughts?

Personally, I think this might just not be the right way to go about this anyway. Maybe<T> isn't usually an open-ended type. You shouldn't need to account for other extensions, so I would turn Maybe into a MaybeBase, and then define Maybe as

type Maybe<T> = Just<T> | Nothing;

@falsandtru
Copy link
Contributor Author

At least,

class A<T> {
    prop: T;
}

class B<T> extends A<T> {
}

class C extends A<any> {
}

var x: A<string> | B<string> | C;
if (x instanceof A) {
    x; // x is C, not A
}

This type resolving is very odd.

@RyanCavanaugh
Copy link
Member

What's a simplified example that results in actual bad behavior inside the if ? I'm seeing a lot of structurally-identical types here so it's hard to distinguish where this would actually matter.

@RyanCavanaugh RyanCavanaugh removed the By Design Deprecated - use "Working as Intended" or "Design Limitation" instead label Jan 25, 2016
@falsandtru
Copy link
Contributor Author

@RyanCavanaugh at least, if that is the design, type of m should be Just<T> | Nothing | Maybe<T>. Nothing | Maybe<T> has no reason.

@DanielRosenwasser

type Maybe<T> = Just<T> | Nothing;

It is right in functional language, but TypeScript is not. Sometimes we must check the instance type instead of pattern matching. TypeScript should provide the way that select the most closest type for alternate method of pattern matching. In your solution, we must check the instance type by m instanceof Just || m instanceof Nothing || m instanceof Maybe(need m instanceof Maybe because this monad adopted Call-by-need evaluation strategy). In my solution, instance type checks is only m instanceof Maybe. This difference for monad need in object oriented language.type Maybe<T> = Just<T> | Nothing does not exist in the runtime of object oriented language, and also m instanceof Just || m instanceof Nothing is not correct because this monad adopted Call-by-need evaluation strategy. Monad types cannot determine for either Just or Nothing in thunk.

@falsandtru
Copy link
Contributor Author

A<T> | C seems a combination of narrowed generics and narrowed non-generics.

I think, instanceof operator must select a most closest type because searching and showing of all derived types is useless and too complex when there are many derived types, and TypeScript should reduce those unnecessary costs for optimization. Additionally, in general, sets of types must narrow by operations, but instanceof operator widen types. Roles and effects of typings for abstraction is reducing of calculation size based on sets size.

@falsandtru
Copy link
Contributor Author

I split this issue: #6607

#6589 (comment) thanks @DanielRosenwasser , I understand. Then I want to use this issue for Type narrowing strategy for most closest type selection.

@falsandtru falsandtru changed the title Narrowing does not work with instanceof operator and type parameters Type narrowing strategy for most closest type selection Jan 25, 2016
@RyanCavanaugh
Copy link
Member

Some short examples to clarify what you mean would be helpful

@falsandtru
Copy link
Contributor Author

Narrowing to the closest runtime type by instanceof operator:

class A<T> {
    prop: T;
}
class B<T> extends A<T> {
}
class C extends B<any> {
}

var x: A<string> | B<any> | C;
if (x instanceof A) {
    x; // closest type is A, now B
}
if (x instanceof B) {
    x; // closest type is B, now B
}
if (x instanceof C) {
    x; // closest type is C, now B
}
if (x instanceof Object) {
    x; // closest type is A, now B
}
if (x instanceof Array) {
    x; // no closest type, must be contextual type `A<string> | B<any> | C`
}

Motivation

Sometimes we must check the instance type instead of pattern matching. TypeScript should provide the way that select the most closest type for alternate method of pattern matching.

// maybe monad
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U> {
    return new Maybe<U>(() => {
      const m: Just<T> | Nothing | Maybe<T> = this.evaluate();
      if (m instanceof Just) {
        return f(m.extract());
      }
      if (m instanceof Nothing) {
        return m;
      }
      if (m instanceof Maybe) {
        return (<Maybe<T>>m).bind(f); // `m` is `Nothing | Maybe<T>`, should be `Maybe<T>`
      }
      throw new TypeError(`ArchStream: Maybe: Invalid monad value.\n\t${m}`);
    });
  }

Searching and showing of all derived types is useless and too complex when there are many derived types, and TypeScript should reduce those unnecessary costs for optimization.

class A<T> {
    prop: T;
}
class B<T> extends A<T> {
}
class C extends B<any> {
}

var x: A<string> | B<any> | C;
if (x instanceof A) {
    x; // this scope narrowed by A, B and C are useless, but x is B
}

In general, sets of types must narrow by operations, but instanceof operator widen types. Roles and effects of typings for abstraction is reducing of calculation size based on sets size.

class A<T> {
    a: T;
}
class B<T> extends A<T> {
    b: T;
}
class C extends A<any> {
    c: any;
}

var x: A<string> | B<string> | C;
if (x instanceof A) {
    x; // x should narrow to A of the most closest type from `instanceof` operator specified type.
    // if you want B or C, it should narrow by those types.
}

instanceof operater compare constructors in js engine, but TypeScript compiler compare structural types. It is mismatch behavior.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 28, 2016

@falsandtru the two examples are actually diffrent. the example using A, B and C is about subtype reduction. your example using Maybe, Nothing and Just, is about control flow.

function f<T>() {
    const m: Just<T> | Nothing | Maybe<T> = this.evaluate();
    if (m instanceof Just) {
       m // Just<T>
    }
    if (m instanceof Nothing) {
        m // Nothing
    }
    else if (m instanceof Maybe) {
        m // Maybe<any>
    }
}

notice the else. the problem is that the return currently does not help narrow the type. #2388 should make this work as intended.

@falsandtru
Copy link
Contributor Author

In my impl, Just and Nothing are subtypes of Maybe for call-by-need evaluation strategy. See #6589 (comment).

@mhegazy
Copy link
Contributor

mhegazy commented May 6, 2016

this essentially the same issue as #7271

@mhegazy mhegazy closed this as completed May 6, 2016
@mhegazy mhegazy added the Duplicate An existing issue was already created label May 6, 2016
@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

4 participants