-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Comments
Why would you want to narrow to |
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. |
I think that's just because we perform subtype reduction within that branch, so you lose |
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? |
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>`
} |
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 Then we'll try to perform subtype reduction. Since What about The issue is that you derived from We'll also try to check if You'll end up with
Actually I'll leave it open. No reason we can't continue discussion here. |
@ahejlsberg @RyanCavanaugh @weswigham any thoughts? Personally, I think this might just not be the right way to go about this anyway. type Maybe<T> = Just<T> | Nothing; |
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. |
What's a simplified example that results in actual bad behavior inside the |
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 |
I think, |
#6589 (comment) thanks @DanielRosenwasser , I understand. Then I want to use this issue for Type narrowing strategy for most closest type selection. |
instanceof
operator and type parameters
Some short examples to clarify what you mean would be helpful |
Narrowing to the closest runtime type by 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`
} MotivationSometimes 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 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.
}
|
@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 |
In my impl, Just and Nothing are subtypes of Maybe for call-by-need evaluation strategy. See #6589 (comment). |
this essentially the same issue as #7271 |
The text was updated successfully, but these errors were encountered: