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

Cannot use differentiating property with generics and inheritance #44733

Closed
marikaner opened this issue Jun 24, 2021 · 3 comments
Closed

Cannot use differentiating property with generics and inheritance #44733

marikaner opened this issue Jun 24, 2021 · 3 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@marikaner
Copy link

marikaner commented Jun 24, 2021

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

⏯ 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>
@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jun 24, 2021
@RyanCavanaugh
Copy link
Member

When going through the conditional type VersionOf, it's technically not safe to substitute in "v1" at infer VersionT when T extends MyClassV1 because some instantiated subtype of T could have a more-specific value of version which 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
Copy link
Author

When going through the conditional type VersionOf, it's technically not safe to substitute in "v1" at infer VersionT when T extends MyClassV1 because some instantiated subtype of T could have a more-specific value of version which would cause the conditional type to be evaluated differently.

I thought I understood this, but:

  • I would expect, that new GenericClassV1('a'); gives me an error as well then.
  • I was not able to build an example that extends MyClassV1 and has a version other than "v1". Example:
class MyClassV2 extends MyClassV1 {
    readonly version: 'v2' = 'v2';
}

gives me this error:

Property 'version' in type 'MyClassV2' is not assignable to the same property in base type 'MyClassV1'.
  Type '"v2"' is not assignable to type '"v1"'.(2416)

What would a "more-specific value of version" be if the super class has a readonly literal string type?

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.

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
Copy link
Member

I would expect, that new GenericClassV1('a'); gives me an error as well then.

There's no ambiguity what new GenericClassV1('a'); is doing; this is different from having a type parameter that is only bounded to be GenericClassV1. Many operations are sound on one but not the other.

"branded primitives"

See #33038

are there any alternatives?

Probably the best I could suggest is

class GenericClassBase<T extends MyClassBase, U extends MyClassBase> {
    constructor(public arg: TypeByVersion<VersionOf<U>>) {}
}

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, MyClassV1> {
    constructor() {
        super('a'); // <----- works as expected ✔
        super('b'); // <----- fails as expected ✔
        super('c'); // <----- works as expected ✔
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants