Open kitsonk opened 8 years ago
@kitsonk this comment and the following discussion I think answers your question
I agree that this is related with #6220 and I think TS should be able to express ES6 Promise subclassing behavior.
@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>
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)
class B<R, T, U> extends A<[C<R, T>]> { ... }
new B<number, string, boolean>().map<Date>(); // <-- what will be here?
FYI #5999
@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..."
@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
So the biggest question currently is what this<T>
will mean for subclasses that have different type parameter length?
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?
@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.
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.
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
}
@SaschaNaz looks like a good solution! +1
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.
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.
@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
.
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.
Checking in since it's been over a year...any new data on this? Are we stuck with intersection types for the indefinite future?
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
?
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: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:
Where if no generics arguments are supplied, it is inferred to be the current ones, where as if they are supplied they are substituted.