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

Polymorphic this and Generics #6223

Open
kitsonk opened this issue Dec 23, 2015 · 20 comments
Open

Polymorphic this and Generics #6223

kitsonk opened this issue Dec 23, 2015 · 20 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@kitsonk
Copy link
Contributor

kitsonk commented Dec 23, 2015

First, thank you team for polymorphic this. It is really handy!

I think I ran into a case though that I am finding challenging, when I need to return a this, but the generics might have changed. For example:

class A<T> {
    private items: T[] = [];
    map<U>(callback: (item: T, idx: number, a: this) => U): this {
        // boring implementation details
    });
}

Where I want to be able to change the generic type for the class with a function, but I will be contracting to return the "current" class, but I want to guard different generics. The following seems logical to me, but doesn't appear to be currently supported:

class A<T> {
    private items: T[] = [];
    map<U>(callback: (item: T, idx: number, a: this) => U): this<U> {
        // boring implementation details
    });
}

Where if no generics arguments are supplied, it is inferred to be the current ones, where as if they are supplied they are substituted.

@myitcv
Copy link

myitcv commented Dec 23, 2015

@kitsonk this comment and the following discussion I think answers your question

@saschanaz
Copy link
Contributor

I agree that this is related with #6220 and I think TS should be able to express ES6 Promise subclassing behavior.

@kitsonk
Copy link
Contributor Author

kitsonk commented Dec 23, 2015

@myitcv thanks, but I don't think it does. I am not talking about inferring higher-order types as seems to be discussed there. I am actually talking about replacing generic type slots with other values in a polymorphic this so that the resulting types can accurately reflect the runtime behaviour of some code.

To express where this becomes a problem, I will extend the class:

class A<T> {
    private items: T[] = [];
    map<U>(callback: (item: T, idx: number, a: this) => U): A<U> {
        // boring implementation details
    });
}

class B<T> extends A<T> {
    foo(): void {};
}

const b = new B<string>().map((item) => Number(item));
// b will be typed as A<number> not B<number>

or

class A<T> {
    private items: T[] = [];
    map<U>(callback: (item: T, idx: number, a: this) => U): this {
        // boring implementation details
    });
}

class B<T> extends A<T> {
    foo(): void {};
}

const b = new B<string>().map((item) => Number(item));
// b will be typed as B<string> not B<number>

@myitcv
Copy link

myitcv commented Dec 23, 2015

This is taken from my comment in that thread:

export interface Iterable<K, V> {
    map<M>(
      mapper: (value?: V, key?: K, iter?: this) => M,
      context?: any
    ): Iterable<K, M>; // can't use 'this' here
}

The comment // can't use 'this' here is exactly what you're trying to solve for, no?

Even though that particular comment doesn't refer to the problem of extending you refer to, if you look further up the thread you'll see this thread was motivated by exactly the same problem (as far as I can tell)

@Igorbek
Copy link
Contributor

Igorbek commented Dec 24, 2015

class B<R, T, U> extends A<[C<R, T>]> { ... }
new B<number, string, boolean>().map<Date>(); // <-- what will be here?

@zpdDG4gta8XKpMCd
Copy link

FYI #5999

@kitsonk
Copy link
Contributor Author

kitsonk commented Dec 24, 2015

@Igorbek I assume you are suggesting the declaration like this:

class A<T> {
    private items: T[] = [];
    map<U>(callback?: (item: T, idx: number, a: this) => U): this {
        // boring implementation details
        return;
    };
}

class B<R, T> extends A<[C<R, T>]> {
    foo(): void {};
}

class C<K, V> {
    map: [K, V][] = [];
}

const b = new B<number, string>().map<Date>();

Which works today and the question you are posing is that because the descent provides a different arity of generics, how would you know what to pass, but this is a problem for current polymorphic this isn't it as well? Because clearly, you can create functions which augment the generics of an interface/class and this does not always represent that properly.

@Aleksey-Bykov plus #5845 and #1213 and #4967. It is a duplicate and I can understand the reasons behind it, which mostly seem to boil down to "well C# doesn't solve this problem either". But of course TypeScript is not C# and it seems to be a hole in the type system... Polymorphic this wasn't a C# feature too, was it?

I am fine if it gets closed as a duplicate, but I am just not sure if we are simply ignoring a problem... Is there a suggestion of another way of expressing the type? The previous answers appear to be summed up as "um, yeah we can't do that..."

@zpdDG4gta8XKpMCd
Copy link

@kitsonk, please find @mhegazy's comment in #5999 where he brings up some non-trivial questions about this feature

please do not confuse it with higher kinded types, they are different

@saschanaz
Copy link
Contributor

So the biggest question currently is what this<T> will mean for subclasses that have different type parameter length?

@saschanaz
Copy link
Contributor

declare class Foo<T> {
    foo(): this<T>;
}
declare class Bar extends Foo<void> { // Error: Foo requires subclasses to have 1 type parameters
}

We can just give an error, no?

@kitsonk
Copy link
Contributor Author

kitsonk commented Dec 24, 2015

@Aleksey-Bykov I think we might be overthinking things, for the sake of edge cases. In my mind it is simple. Polymorphic this would have the same arity of generics as the class that it was declared in. If someone was "stupid" and somehow blatted the underlying generic and that didn't manifest itself in the descendent class, it is almost immaterial, because that is what happens with polymorphic this and generics anyways. So to go back to the original problem:

class A<T> {
    map1<U>(callback: (item: T, idx: number, a: this) => U): this<U>;
    map1<U>(callback: (item: T, idx: number, a: this) => U): A<U>;
    map2<U>(callback: (item: T, idx: number, a: this) => U): this;
}

class B<K, V> extends A<[K, V]> {
}

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

const b1 = new B<string, number>().map1<boolean>(); // Type is B<string, number>
const b2 = new B<string, number>().map2<boolean>(); // Type is A<boolean>
const b3 = new B<string, number>().map3<boolean>(); // Type is B<string, number>

const c1 = new C<string>().map1<boolean>(); // Type is C<boolean>
const c2 = new C<string>().map2<boolean>(); // Type is A<boolean>
const c3 = new C<string>().map3<boolean>(); // Type is C<string>

Of course B is a problem, but it is a problem no matter what... none of the results accurately describe the intent of the code and some sort of type coercion is required. At least with C, we can accurately describe the intent of the code.

In my mind all we are is choosing our own flavour of stupidity. I prefer the one where polymorphic this can take arguments where the arity of the generics does not change in subclasses.

@zpdDG4gta8XKpMCd
Copy link

In my mind all we are is choosing our own flavour of stupidity. I prefer the one where polymorphic this can take arguments where the arity of the generics does not change in subclasses.

I've seen a lot of people who change the arity in a subclass all the time.

class Base<Dont, Know, Anything, Yet> { }
class Intermediate<More, Unknowns> extends Base<Oh, Yes, Now, ItsClear> { }
class Final<One, Last, Thing> extends Intermediate<All, Known> { }

There will be a storm of issues with questions why the arity can't be changed just like now we see people puzzled why this can't be generic.

Bottom line is that the current syntax isn't capable enough to make this feature consistent. New syntax is required. New syntax is a big deal. The feature needs a strong justification and crave from the users. Not saying it's not possible and won't ever happen, my personal take is that HKT's are a better investment as they are more fundamental to the type system than this with type parameters.

@saschanaz
Copy link
Contributor

A short brainstorming without HKT:

declare class Base<T> {
    property: T;
    method<U>(): this<U>; // works as implicit type parameter restriction
}
declare class Subclass1<T, U> extends Base<U /* type parameter linked */> {
    // property: U;
    // method<V>(): Subclass<T, V /* linked */>;
}
declare class Subclass2<T, U> extends Base<void> {
    // property: void;
    // method<V>(): ?? <- Error, subclass should have linked type parameter
}
declare class Subclass3<T> extends Base<void> {
    // property: void;
    // method<V>(): ?? <- Error, subclass should have linked type parameter
}
declare class Subclass4 extends Base<void> {
    // property: void;
    // method<V>(): ?? <- Error, subclass should have linked type parameter
}

@niieani
Copy link

niieani commented Jan 30, 2016

@saschanaz looks like a good solution! +1

@mhegazy mhegazy added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Feb 19, 2016
@electricessence
Copy link

electricessence commented Jun 12, 2016

Yes. I'm dealing with this issue as well.
I would prefer 'this' to represent "this type" instead of asserting that it will be 'this'.
And it would be nice to be able to map directly to the current type and pass different generic type params.

I have now changed my code to use :this where it really makes the simplest sense.
But I've also went forward and changed the return type to :this and then simply forced the result with <any> because I knew the return type was the same. This could be bad in the future if :this did deeper level inspections that asserted non-null tree flows, etc.

Without using :this I clearly have to have more complex class structures and override methods just to ensure the types align.

@dead-claudia
Copy link

Note: allowing a generic this could be simulated this way now, using a parameter for what you're adding to it:

declare function use<T>(foo: Foo<T>): void
interface Foo<T> extends T {
    one: string
}

const foo = {
    one: "one",
    two: 2,
}

use(foo) // works now
use<{two: number}>(foo) // equivalent

If you were to allow extends this in all generic contexts, it could just be made redundant in interface extension (everything is already a structural subtype of themselves), so it's possible to add to the type system soundly.

@variousauthors
Copy link

variousauthors commented Apr 29, 2018

@isiahmeadows when I try that code snippet in VSCode I get an error message under the extends T to the tune of An interface may only extend a class or another interface.

@dead-claudia
Copy link

Yeah, they fixed the inconsistency. The workaround is to use type intersection, but IMHO it's not a great workaround given it prevents me from assuming this extends T (which is actually useful on occasion, believe it or not), and it's forced me to recast types and parts of my data model at the type level more than once, in ways that were just boilerplatey hacks. I just wish they would allow extends T and extends this in an F-bounded fashion - it'd make certain type-heavy modeling cases so much easier.

@rdhelms
Copy link

rdhelms commented Nov 27, 2019

Checking in since it's been over a year...any new data on this? Are we stuck with intersection types for the indefinite future?

@fan-tom
Copy link

fan-tom commented Dec 24, 2019

A short brainstorming without HKT:

declare class Base<T> {
    property: T;
    method<U>(): this<U>; // works as implicit type parameter restriction
}
declare class Subclass1<T, U> extends Base<U /* type parameter linked */> {
    // property: U;
    // method<V>(): Subclass<T, V /* linked */>;
}
declare class Subclass2<T, U> extends Base<void> {
    // property: void;
    // method<V>(): ?? <- Error, subclass should have linked type parameter
}
declare class Subclass3<T> extends Base<void> {
    // property: void;
    // method<V>(): ?? <- Error, subclass should have linked type parameter
}
declare class Subclass4 extends Base<void> {
    // property: void;
    // method<V>(): ?? <- Error, subclass should have linked type parameter
}

what about that?

declare class Base<T> {
    property: T;
    method<U>(): this<U>; // works as implicit type parameter restriction
}
declare class Subclass1<T, U> extends Base<U /* type parameter linked */> {
    // property: U;
    // method<V>(): Subclass<T, V /* linked */>;
}
declare class Subclass2<T, U> extends Base<void> {
    // property: void;
    // method<V>(): Subclass2<T, U> (extends Base<V>), so property type changed from void to V
}
declare class Subclass3<T> extends Base<void> {
    // property: void;
    // method<V>(): Subclass3<T> (extends Base<V>), so property type changed from void to V
}
declare class Subclass4 extends Base<void> {
    // property: void;
    // method<V>(): Subclass4<T> (extends Base<V>), so property type changed from void to V
}

Can we change base class type parameters when we return this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests