You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Consider base class from extended generic type #2441
Sorry if this has already been submitted (was not able to find).
It would be great if TS could consider the base class from an extended generic type.
interface Type<T> {
new (...args: any[]): T;
}
function ok(type: Type<Error>) {
return type == RangeError;
}
function fail<T extends Error>(type: Type<T>) {
return type == RangeError; // Operator '==' cannot be applied to types 'Type<T>' and 'RangeErrorConstructor'.
}
Note that this is not my real code, but just a very basic example showing the issue (in Playground for example).
In ok(), TS correctly considers RangeError as a possible value of type (if we put a type that does not extend Error, e.g. String, TS correctly reports an error).
In fail(), TS seems to have no knowledge of T (or Type), and rejects all comparisons, even the following:
function fail<T>(type: Type<T>) {
return type == Object; // Operator '==' cannot be applied to types 'Type<T>' and 'ObjectConstructor'.
}
My last sentence is confusing...
In the last code example, this is normal that TS raises this error, as it doesn't know what T can be, whereas in my initial comment, TS should consider T at least of type Error.
In this case, you could end up comparing RangeError to OtherError. But they will never be comparable because RangeErrorConstructor has a prototype property that Type<OtherError> doesn't have, and Type<OtherError> has an otherProperty that RangeErrorConstructor doesn't have.
So the fact that T is at least Error is not sufficient to ensure that they are comparable. That is why the compiler does not allow it.
The code can only work with class types, not interfaces.
Anyway, forget about the example with error classes. Here is another example with my own classes which compiles:
class A { a: number; }
class B extends A { b: number; }
class C extends A { c: number; }
function isB(type: Type<A>) {
return type == B;
}
console.log(isB(A)); // false
console.log(isB(B)); // true
console.log(isB(C)); // false
The issue I'm reporting is linked to the way I declare the function.
The following does not compile:
function isB<T extends A>(type: Type<T>) {
return type == B; // Operator '==' cannot be applied to types 'Type<T>' and 'typeof B'.
}
I indeed confirm this test case has no interest, but I wanted to provide a very short example that highlights the issue.
My real case is an event/listener mechanism, where the type of events is based on classes.
For example, the DOM addEventListener() would be reduced to a unique definition:
But I think your objection is that this feels silly. It seems like this is a perfectly reasonable equality comparison, as you are comparing values of seemingly related types. But the type system does not see it that way. For example, you would not want to be allowed to assign EvaluateEvent to type. So the type system requires that you mention some other type that is a supertype of both (in this case Type<Event>).
I don't think the fix here is to treat T as synonymous with Event as you suggest. We do actually do that when checking that type is assignable to EvaluateEvent, but not when checking that EvaluateEvent is assignable to type. And you wouldn't want that.
In my opinion, this issue is related to #1447, which exhibits a similar problem for casts, where this one is for equality comparisons. The gist is that there are some operators that would benefit from a looser relation than assignability, a relation that asks "Are these two types vaguely related, even if one is not certainly assignable to the other?". And I think your issue suggests firstly that equality comparisons are one such operator. And secondly that this relation may have to consider type parameters synonymous with their constraints on both sides.
I suppose I see your point, but I don't think it is related to #1447.
In #1447, Foo and { a: undefined } are different in both directions.
In my case, EvaluateEvent is assignable to Event.
Let's restart with the following code, closer to my real case, though simpler, with 3 implementations of listen() (can be copied/pasted to Playground):
module A {
interface Type<T> { new (...args: any[]): T; }
interface Listener<T> { (event: T): void; }
class Event { a: number; }
class SetValueEvent extends Event { b: number; }
class EvaluateEvent extends Event { c: number; }
function listen1(type: Type<Event>, listener: Listener<Event>): void {
if (type == EvaluateEvent) { }
}
function listen2<T extends Event>(type: Type<T>, listener: Listener<T>): void {
if (<Type<Event>>type == EvaluateEvent) { } // Need to cast.
}
function listen3<T extends EvaluateEvent>(type: Type<T>, listener: Listener<T>): void {
if (type == EvaluateEvent) { } // No need to cast.
}
}
What I don't understand is the difference between:
an allowed call to listen1(SetValueEvent, ...) where the condition will always be false,
for listen2(), the fact that TS doesn't (want to?) consider that type is at least the Event class/constructor,
for listen3(), TS doesn't complain whereas T could be a sub-class of EvaluateEvent, so same situation as for listen2() to me.
(knowing that in the 3 situations, the comparison is always valid whatever the types, we're comparing 2 constructor functions).
If the comparison from B to C were allowed because A exists, then any comparison would be allowed because there is always some A (namely, {}).
When you have a generic constraint, e.g. T extends A, the compiler assumes that T is more like a B or C than an A. This is because code like this should not compile:
functionfoo<TextendsAnimal>() : T{returnnewDog();// Dog extends Animal, just like T!}varx=foo<Cat>();// x: Cat but is actually a Dog, wat?
In your example, this all maps to the diagram above:
Event is Type<A>
Type<T> where T extends Event starts at Type<A> but is treated like Type<B> (see above)
Type<C> is EvaluateEvent
Type<T> where T extends EvaluateEvent starts at Type<C> but is treated like Type<D>
listen1 is OK because comparisons between A and B are OK listen3 is OK because comparisons between C and D are OK
listen2 is not OK because comparisons between B and C are not allowed.
The key thing to realize in your example is the following:
In listen1: Every Type<EvaluateEvent> is a Type<Event>, so you can compare type to EvaluateEvent.
In listen3: Every Type<T> (where T extends EvaluateEvent) is a Type<EvaluateEvent>, so you can compare them.
In listen2: Not every Type<T> (where T extends Event) is a Type<EvaluateEvent>, nor is every Type<EvaluateEvent> a type Type<T>, so you cannot do the comparison.
Thanks for your answers.
I was too much focused on the very particular case that Type<T> is just a function (the constructor of T, so any function can be compared to any function, whatever T), whereas I understand that TS needs to keep this distinction for all other cases.
Activity
JsonFreeman commentedon Mar 21, 2015
Your example references RangeError. But what is the definition of RangeError?
stephanedr commentedon Mar 21, 2015
RangeError is the standard JavaScript type, which extends Error, as defined in lib.d.ts:
Note that this is not my real code, but just a very basic example showing the issue (in Playground for example).
In
ok(), TS correctly considers RangeError as a possible value oftype(if we put a type that does not extend Error, e.g. String, TS correctly reports an error).In
fail(), TS seems to have no knowledge of T (or Type), and rejects all comparisons, even the following:stephanedr commentedon Mar 21, 2015
My last sentence is confusing...
In the last code example, this is normal that TS raises this error, as it doesn't know what T can be, whereas in my initial comment, TS should consider T at least of type Error.
JsonFreeman commentedon Mar 21, 2015
But T could be some other subtype of Error. What if you had the following:
In this case, you could end up comparing RangeError to OtherError. But they will never be comparable because RangeErrorConstructor has a prototype property that
Type<OtherError>doesn't have, andType<OtherError>has an otherProperty that RangeErrorConstructor doesn't have.So the fact that T is at least Error is not sufficient to ensure that they are comparable. That is why the compiler does not allow it.
stephanedr commentedon Mar 21, 2015
The code can only work with class types, not interfaces.
Anyway, forget about the example with error classes. Here is another example with my own classes which compiles:
The issue I'm reporting is linked to the way I declare the function.
The following does not compile:
JsonFreeman commentedon Mar 21, 2015
I guess there are two things I don't understand:
isB(C)if it is statically guaranteed to be false? You might as well just writefalse.isBto be generic? There seems to be no benefit to make it generic.stephanedr commentedon Mar 22, 2015
I indeed confirm this test case has no interest, but I wanted to provide a very short example that highlights the issue.
My real case is an event/listener mechanism, where the type of events is based on classes.
For example, the DOM
addEventListener()would be reduced to a unique definition:instead of the >100 declarations in lib.d.ts (but would need as many classes as possible types...).
The problem I have is in one of the add listener overrides, I need to compare the given
typeto a particular event class (to perform extra stuff).JsonFreeman commentedon Mar 23, 2015
Ah I see. That information helps.
You can cast to
<Type<Event>>to retain some type safety. And you can put that cast either ontypeor onEvaluateEvent.Alternatively, you don't have to cast, you can do something like this inside the listen function:
But I think your objection is that this feels silly. It seems like this is a perfectly reasonable equality comparison, as you are comparing values of seemingly related types. But the type system does not see it that way. For example, you would not want to be allowed to assign
EvaluateEventtotype. So the type system requires that you mention some other type that is a supertype of both (in this caseType<Event>).I don't think the fix here is to treat
Tas synonymous withEventas you suggest. We do actually do that when checking that type is assignable to EvaluateEvent, but not when checking that EvaluateEvent is assignable to type. And you wouldn't want that.In my opinion, this issue is related to #1447, which exhibits a similar problem for casts, where this one is for equality comparisons. The gist is that there are some operators that would benefit from a looser relation than assignability, a relation that asks "Are these two types vaguely related, even if one is not certainly assignable to the other?". And I think your issue suggests firstly that equality comparisons are one such operator. And secondly that this relation may have to consider type parameters synonymous with their constraints on both sides.
stephanedr commentedon Mar 23, 2015
I suppose I see your point, but I don't think it is related to #1447.
In #1447,
Fooand{ a: undefined }are different in both directions.In my case,
EvaluateEventis assignable toEvent.Let's restart with the following code, closer to my real case, though simpler, with 3 implementations of
listen()(can be copied/pasted to Playground):What I don't understand is the difference between:
listen1(SetValueEvent, ...)where the condition will always be false,listen2(), the fact that TS doesn't (want to?) consider thattypeis at least theEventclass/constructor,listen3(), TS doesn't complain whereas T could be a sub-class of EvaluateEvent, so same situation as forlisten2()to me.(knowing that in the 3 situations, the comparison is always valid whatever the types, we're comparing 2 constructor functions).
RyanCavanaugh commentedon Mar 23, 2015
So imagine this diagram of types:
You can do the following sorts of comparisons:
A == BC == DA == DBut not
B == CIf the comparison from
BtoCwere allowed becauseAexists, then any comparison would be allowed because there is always someA(namely,{}).When you have a generic constraint, e.g.
T extends A, the compiler assumes thatTis more like aBorCthan anA. This is because code like this should not compile:In your example, this all maps to the diagram above:
EventisType<A>Type<T>whereT extends Eventstarts atType<A>but is treated likeType<B>(see above)Type<C>isEvaluateEventType<T>whereT extends EvaluateEventstarts atType<C>but is treated likeType<D>listen1is OK because comparisons betweenAandBare OKlisten3is OK because comparisons betweenCandDare OKlisten2is not OK because comparisons betweenBandCare not allowed.JsonFreeman commentedon Mar 23, 2015
The key thing to realize in your example is the following:
Type<EvaluateEvent>is aType<Event>, so you can comparetypetoEvaluateEvent.Type<T>(where T extends EvaluateEvent) is aType<EvaluateEvent>, so you can compare them.Type<T>(where T extends Event) is aType<EvaluateEvent>, nor is everyType<EvaluateEvent>a typeType<T>, so you cannot do the comparison.stephanedr commentedon Mar 24, 2015
Thanks for your answers.
I was too much focused on the very particular case that
Type<T>is just a function (the constructor of T, so any function can be compared to any function, whatever T), whereas I understand that TS needs to keep this distinction for all other cases.