-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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' type #4910
Polymorphic 'this' type #4910
Conversation
@jbondc Agreed, it might be good to show which class a particular |
👏 |
Conflicts: src/compiler/diagnosticInformationMap.generated.ts
for (let node of baseTypeNodes) { | ||
if (isSupportedExpressionWithTypeArguments(node)) { | ||
let baseSymbol = resolveEntityName(node.expression, SymbolFlags.Type, /*ignoreErrors*/ true); | ||
if (!baseSymbol || !(baseSymbol.flags & SymbolFlags.Interface) || getDeclaredTypeOfClassOrInterface(baseSymbol).thisType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this handle the case where an interface extends
a class
that uses this
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If one of the base types is a class (i.e. not an interface) we simply return true (because every class has a this
type). We only return false from this function if the interface itself doesn't reference this, if every base type is an interface, and if no base interface has a this
type.
Need to un-comment the fourslash tests that were edited |
@RyanCavanaugh Yes, @mhegazy is looking at fixes for the failing fourslash tests. |
@@ -119,6 +145,8 @@ tests/cases/conformance/expressions/thisKeyword/typeOfThis.ts(130,16): error TS1 | |||
!!! error TS1056: Accessors are only available when targeting ECMAScript 5 and higher. | |||
var p = this; | |||
var p: MyGenericTestClass<T, U>; | |||
~ | |||
!!! error TS2403: Subsequent variable declarations must have the same type. Variable 'p' must be of type 'this', but here has type 'MyGenericTestClass<T, U>'. | |||
p = v; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally these should not fail right? But only on instantiation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, they should fail. The first declaration infers type this
for v
, the second declaration specifies type MyGenericTestClass<T, U>
for v
. Those are not identical types, so error.
@ahejlsberg I spoke with @RyanCavanaugh, @weswigham, and @danquirk about this, and I think there are some issues with the meaning of interface Thing {
getResource(): {
methodA(): this;
methodB(): this;
}
} In the current implementation, For either one semantics of the However, if interface Thing {
getResource<OwnerT extends Thing>(): {
a(): OwnerT;
b(): this;
}
} I think that we should go with the semantics that most-closely matches the runtime: in this case, the |
@DanielRosenwasser You're incorrect about the current behavior, your example is actually an error: interface Thing {
getResource(): {
methodA(): this; // Error
methodB(): this; // Error
}
} The error reported is In order to reference an outer interface Resource<T> {
methodA(): T;
methodB(): T;
}
interface Thing {
getResource(): Resource<this>;
} Note, however, that you can capture the outer this in an object literal: class A {
foo() {
return { x: this, f: () => this };
}
} In order to write down the type inferred for the above, you'd have to resort to a temporary type: interface Foo<T> { x: T; f(): T; }
class A {
foo(): Foo<this> {
return { x: this, f: () => this };
}
} |
@jbondc I think the |
I actually was asking others about the implementation and it was our understanding that it was allowed; sorry about that. I didn't get the chance to try the branch out yesterday.
But the .d.ts emit for that is currently not valid: declare class A {
foo(): {
x: this;
f: () => this;
};
} If I try to reuse that
|
@DanielRosenwasser @mhegazy The .d.ts issue is tricky. Personally I think this is one of those situations where we should issue an error if a .d.ts is requested with guidance that a type annotation is required. |
return true; | ||
} | ||
|
||
// A type is considered independent if it is a built-in type keyword, an array with an element type that is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe clarify that this
is not a built-in type keyword.
As I understand, polymorphic // interface Cloneable<T extends Cloneable<T>> {
interface Cloneable<T extends Cloneable<any>> {
clone(): T;
} turns it into interface Cloneable {
clone(): this;
} It's cool! However It looks like that it solves only a half of a problem (ok, let it be 90% in common practices) Am I right, that the system // interface Vertex<V extends Vertex<V,E>, E extends Edge<V,E>> {
interface Vertex<V extends Vertex<any,any>, E extends Edge<any,any>> {
incoming: E[];
outgoing: E[];
}
//interface Edge<V extends Vertex<V,E>, E extends Edge<V,E>> {
interface Edge<V extends Vertex<any,any>, E extends Edge<any,any>> {
from: V;
to: V;
}
class City extends Vertex<City, Road> {
constructor(public name: string){
this.incoming = [];
this.outgoing = [];
}
}
class Road extends Edge<City, Road> {
constructor(public distance: number, public from: City, public to: City) {
this.from.outgoing.push(this);
this.to.incoming.push(this);
}
}
var LA = new City('Los Angeles');
var SF = new City('San Francisco');
var hyperloop = [new Road(558.68, LA, SF), new Road(558.68, SF, LA)]
console.log(hyperloop[0].from.outgoing[0].to.name) // stronly typed still could not benefit from I could imagine the following implementation // interface Vertex<E<T> extends Edge<T>> -or-
interface Vertex<E extends Edge> {
incomming: E<this>[];
outgoing: E<this>;
}
// interface Edge<V<T> extends Vertex<T>> -or-
interface Edge<V extends Vertex> {
from: V<this>;
to: V<this>;
}
class City extends Vertex<Road> { ... }
class Road extends Edge<City> { ... } where for high order types the constraint
Am I right? Or there is a simpler solution that I've missed? |
I mean that |
@Artazor Correct, the polymorphic |
🎉 👍 |
should this also support this syntax?
|
This seems to cause the following issue. In the example below in the Is this expected?
|
@mjohnsonengr this is the intended behavior. If there were a subclass of I'd recommend writing |
Nevermind, saw the referenced issue, #5056 |
Ahh, but thank your for your reply @RyanCavanaugh! :) |
so what should I do to use this instead of state below function NavigatableRecordCreator(defaultValues: { [key: string]: any; }, name?: string) {
abstract class NavigatableRecord<P extends NavigatableRecord<P>> extends Record(defaultValues, name) {
SetValue<T>(fn: (x: NavigableObject<P>) => NavigableObject<T>, value: T, state: P) {
return this.setIn(fn(new NavigableObject<P>(state)).getPath(), value) // can I use this instead of state here
}
}
return NavigatableRecord;
} |
As I wrote in #3694, how can polymorphic |
This PR implements polymorphic typing of
this
for classes and interfaces as inspired by #3694. The new features are:this
in an expression within a non-static class or interface member is considered to be an instance of some class that derives from the containing class as opposed to simply an instance of the containing class.this
keyword can be used in a type position within a non-static class or interface member to reference the type ofthis
.this
type within the class (including those inherited from base classes) are replaced with the type itself.This feature makes patterns such as fluent interfaces and covariant return types much easier to express and implement. Languages such as C++, Java, and C# use a somewhat involved generic pattern to emulate this feature, as described here.
An example:
In a non-static member of a class or interface,
this
in a type position refers to the type ofthis
. For example:Each subclass of the above
Entity
will have aclone
method that returns an instance of the subclass, and anequals
method that takes another instance of the subclass.The
this
type is a subtype of and assignable to the instance type of the containing class or interface, but not vice-versa (becausethis
might actually be a subclass). That is a breaking change, and certain code patterns that previously compiled may now need an extra type annotation:The example above now errors because the inferred return type of
getInstance
inA
isthis
and the inferred type ofgetInstance
inB
isB
, which is not assignable tothis
. The fix is to add a return type annotation forgetInstance
inA
.The polymorphic
this
type is implemented by providing every class and interface with an implied type parameter that is constrained to the containing type itself (except when the compiler can can tell thatthis
is never referenced within the class or interface). That in particular turns a lot of previously non-generic classes into generic equivalents, causing more symbols and types to be created due to generic instantiation. The observed cost in batch compile time ranges from 0% in code that uses no classes to 6-7% in very class-heavy code.Note that this PR specifically doesn't aim to implement other parts of #3694 such as
this
type annotations for functions. Those will be covered by other PRs if we choose to implement them.