Repository navigation
Cannot use differentiating property with generics and inheritance #44733
Copy link
Copy link
Closed
Labels
Design LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
Description
Bug Report
In my code I want to differentiate allowed types based on a differentiating property (version in my example).
Some classes have this property set, other (generic) classes should accept certain values based on the classes with this property.
This works, as long as I don't introduce hierarchies between the generic classes.
With hierarchies, only values that relate to all differentiating properties (i.e., common values) can be set (see code below).
Other values fail with:
Argument of type '"a"' is not assignable to parameter of type 'TypeByVersion<VersionOf<T>>'.(2345)
🔎 Search Terms
generics, inheritance
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about (Generic conditional type T extends never ? 'yes' : 'no' resolves to never when T is never. #31751). But this wasn't related. I think.
⏯ Playground Link
Playground link with relevant code
💻 Code
/* Version to differentiate on */
type Version = 'v1' | 'v2';
/* Types by version */
type Common = 'c';
type V1 = 'a';
type V2 = 'b';
type TypeByVersion<T extends 'v1' | 'v2' | 'any'> = T extends 'v1'
? Common | V1
: T extends 'v2'
? Common | V2
: Common | V1 | V2;
/* Classes with a property to differentiate on */
abstract class MyClassBase {
constructor(public name: string) {}
}
// Version 1
abstract class MyClassV1 extends MyClassBase {
readonly version: 'v1' = 'v1';
}
// Version 2
abstract class MyClassV2 extends MyClassBase {
readonly version: 'v2' = 'v2';
}
/* Type to infer the differentiating property */
type VersionOf<T extends MyClassBase> = T extends {
version: infer VersionT;
}
? VersionT
: 'any';
/* Classes using the classes above in generics */
/* Case 1: Without base class, directly referencing MyClassV1 */
class GenericClassV1<T extends MyClassV1> {
constructor(public arg: TypeByVersion<VersionOf<T>>) {}
}
new GenericClassV1('a'); // <----- works as expected ✔
new GenericClassV1('b'); // <----- fails as expected ✔
new GenericClassV1('c'); // <----- works as expected ✔
/* Case 2: With base class, referencing MyClassBase in the base class, but MyClassV1 in the specific class */
class GenericClassBase<T extends MyClassBase> {
constructor(public arg: TypeByVersion<VersionOf<T>>) {}
}
new GenericClassBase('a'); // <----- works as expected ✔
new GenericClassBase('b'); // <----- works as expected ✔
new GenericClassBase('c'); // <----- works as expected ✔
class GenericClassV1Ext<T extends MyClassV1> extends GenericClassBase<T> {
constructor() {
super('a'); // <----- fails, but not expected ❌
super('b'); // <----- fails as expected ✔
super('c'); // <----- works as expected ✔
}
}🙁 Actual behavior
Calling the super constructor from GenericClassV1Ext with 'a' fails.
🙂 Expected behavior
I would expect calling the super constructor from GenericClassV1Ext with 'a' works, because it works for both other cases:
GenericClassV1<T extends MyClassV1>GenericClassBase<T extends MyClassBase>
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Design LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
Activity
RyanCavanaugh commentedon Jun 24, 2021
When going through the conditional type
VersionOf, it's technically not safe to substitute in"v1"atinfer VersionTwhenT extends MyClassV1because some instantiated subtype ofTcould have a more-specific value ofversionwhich would cause the conditional type to be evaluated differently.You could use some sort of counterfactual reasoning of the sort "No legal subtype of MyClassV1 could have a more-more specific type than a unit type", but that assumption precludes further subdivision of
"v1"into e.g. branded primitives, which is a popular feature request.marikaner commentedon Jun 25, 2021
I thought I understood this, but:
new GenericClassV1('a');gives me an error as well then.MyClassV1and has aversionother than"v1". Example:gives me this error:
What would a "more-specific value of
version" be if the super class has a readonly literal string type?I assume my reasoning above is exactly what you mean by this. But how is it counterfactual? As far as I understand, as of today
"v1"cannot be further subdivided - therefore factual. Do you have a reference on "branded primitives" - a quick google and GH issue search only yielded ambiguous results.That said, assuming that the way I am trying to achieve this just is incorrect, are there any alternatives?
RyanCavanaugh commentedon Jun 25, 2021
There's no ambiguity what
new GenericClassV1('a');is doing; this is different from having a type parameter that is only bounded to beGenericClassV1. Many operations are sound on one but not the other.See #33038
Probably the best I could suggest is