How to wrap a class that has generic type methods with a class that has a generic type and no generic type arguments on the methods?

huangapple go评论46阅读模式
英文:

How to wrap a class that has generic type methods with a class that has a generic type and no generic type arguments on the methods?

问题

I have the following code example:

class Stupid {
  private cache: Map<any, any> = new Map();
  get<T>(key: string): T {
    return this.cache.get(key);
  };
}

class Smart<T> extends Stupid {
  get(key: string): T {
    super.get<T>(key);
  }
}

I have valid reasons for wrapping the Stupid class (other than the generics) which are not apparent in this barebone reproduction of the problem, nonetheless I would like to know if this is somehow possible.

Please also note that the class Stupid is a node dependency. I can't change that implementation.

The purpose would be to not have a generic type argument on each method but to move it to the wrapping class (class Smart) and use that in super calls, to provide the required generic to the extended class (class Stupid).

英文:

I have the following code example:

class Stupid {
  private cache: Map&lt;any, any&gt; = new Map();
  get&lt;T&gt;(key: string): T {
    return this.cache.get(key);
  };
}

class Smart&lt;T&gt; extends Stupid {
  get(key: string): T {
    super.get&lt;T&gt;(key);
  }
}

I have valid reasons for wrapping the Stupid class (other than the generics) which are not apparent in this barebone reproduction of the problem, nonetheless I would like to know if this is somehow possible.

Please also note that the class Stupid is a node dependency. I can't change that implementation.

The purpose would be to not have a generic type argument on each method, but to move it to the wrapping class (class Smart) and use that in super calls, to provide the required generic to the extended class (class Stupid).

Please feel free to use this playground for experimentation.

答案1

得分: 0

以下是要翻译的内容:

The get() method in Stupid has the problematic generic call signature <T>(key: string) => T, which means that it will return a value of any type T that the caller specifies. This is demonstrably impossible:

Stupid中的get()方法具有有问题的泛型 generic 调用签名 <T>(key: string) => T,这意味着它将返回调用者指定的任何类型T的值。这显然是不可能的:

const stupid = new Stupid();
const n = stupid.get<number>("xyz");
// const n: number
const s = stupid.get<string>("xyz");
// const s: string

In the above, you're calling stupid.get("xyz") twice at runtime, but the compiler thinks the first call returns a number and the second one returns a string, which is exceptionally unlikely. The only reason the method implementation type checks is because the return type of cache.get() is the intentionally unsafe any type.

在上面的例子中,你在运行时两次调用了stupid.get("xyz"),但编译器认为第一次调用返回一个number,第二次调用返回一个string,这是非常不可能的。该方法的实现类型检查通过的唯一原因是cache.get()的返回类型故意是不安全的any类型

Let's not worry too much about the type safety of Stupid.get() and instead look at the problem you're having when subclassing it:

让我们不要过于担心Stupid.get()的类型安全性,而是看看在子类化时遇到的问题:

class Smart<T> extends Stupid {
  get(key: string): T { // error!
    return super.get<T>(key);
  }
}

const smart: Smart<number> = new Smart();
const n = smart.get("xyz");
// const n: number

The reason that doesn't work is because subclasses must be assignable to their superclasses. If Smart<T> extends Stupid, then every Smart<T> instance is also a Stupid instance, and should be usable accordingly:

这不起作用的原因是因为子类必须可分配给其超类。如果Smart<T> extends Stupid,那么每个Smart<T>实例也是Stupid实例,并应该相应地可用:

const stupid: Stupid = smart; // this should be allowed
const s = stupid.get<string>("xyz");
// const s: string // uh oh

Smart<number>.get() returns a number, but Stupid.get() returns any type the caller wants, such as string. These are not compatible behaviors, and so the compiler complains. If you want Smart to truly be a subclass of Stupid, you'd need to make get() generic the same way as it is in Stupid, which is not what you're trying to do.

Smart<number>.get()返回number,但Stupid.get()返回调用者想要的任何类型,例如string。这些行为不兼容,因此编译器会报错。如果你希望Smart真正成为Stupid的子类,你需要像在Stupid中一样使get()成为泛型,这不是你的意图。

So how can we proceed? Presumably we shouldn't worry too much about type safety, since there's no way to guarantee that get() returns the proper type for either Stupid or Smart. Instead we will just do what's necessary to make it compile. Usually that will require something like a type assertion to tell the compiler that some value is of some type.

那么我们应该如何继续?我们大概不应该太担心类型安全性,因为没有办法保证get()StupidSmart返回正确的类型。相反,我们将只需执行必要的步骤使其编译通过。通常情况下,这将需要类似于类型断言的东西,以告诉编译器某个值是某种类型。

Here's one way to do it:

以下是一种方法:

interface Smart<T> {
  get(key: string): T
}

const FakeSmart = Stupid as new <T>() => ISmart<T>;

class Smart<T> extends FakeSmart<T> {
  get(key: string): T {
    return super.get(key);
  }
}

What we're doing is pretending that Stupid behaves like Smart. First we create an ISmart<T> interface which is the same as Stupid except that the generic is moved to where you want it. Then we assign the Stupid constructor to a new variable called FakeSmart and assert that FakeSmart has the type of a generic constructor. So FakeSmart is just Stupid at runtime, but the compiler thinks it is of a proper superclass of Smart<T>.

我们正在假装Stupid的行为类似于Smart。首先,我们创建一个ISmart<T>接口,与Stupid相同,只是将泛型移到你想要的位置。然后,我们将Stupid构造函数分配给一个名为FakeSmart的新变量,并断言FakeSmart具有泛型构造函数的类型。因此,在运行时,FakeSmart只是Stupid,但编译器认为它是Smart<T>的适当超类。

And then we declare that Smart<T> extends FakeSmart<T>. At runtime this is the

英文:

The get() method in Stupid has the problematic generic call signature &lt;T&gt;(key: string) =&gt; T, which means that it will return a value of any type T that the caller specifies. This is demonstrably impossible:

const stupid = new Stupid();
const n = stupid.get&lt;number&gt;(&quot;xyz&quot;);
// const n: number
const s = stupid.get&lt;string&gt;(&quot;xyz&quot;);
// const s: string

In the above, you're calling stupid.get(&quot;xyz&quot;) twice at runtime, but the compiler thinks the first call returns a number and the second one returns a string, which is exceptionally unlikely. The only reason the method implementation type checks is because the return type of cache.get() is the intentionally unsafe any type.

Let's not worry too much about the type safety of Stupid.get() and instead look at the problem you're having when subclassing it:


class Smart&lt;T&gt; extends Stupid {
  get(key: string): T { // error!
    return super.get&lt;T&gt;(key);
  }
}

const smart: Smart&lt;number&gt; = new Smart();
const n = smart.get(&quot;xyz&quot;);
// const n: number

The reason that doesn't work is because subclasses must be assignable to their superclasses. If Smart&lt;T&gt; extends Stupid, then every Smart&lt;T&gt; instance is also a Stupid instance, and should be usable accordingly:

const stupid: Stupid = smart; // this should be allowed
const s = stupid.get&lt;string&gt;(&quot;xyz&quot;); 
// const s: string // uh oh

Smart&lt;number&gt;.get() returns a number, but Stupid.get() returns any type the caller wants, such as string. These are not compatible behaviors, and so the compiler complains. If you want Smart to truly be a subclass of Stupid, you'd need to make get() generic the same way as it is in Stupid, which is not what you're trying to do.


So how can we proceed? Presumably we shouldn't worry too much about type safety, since there's no way to guarantee that get() returns the proper type for either Stupid or Smart. Instead we will just do what's necessary to make it compile. Usually that will require something like a type assertion to tell the compiler that some value is of some type.

Here's one way to do it:

interface Smart&lt;T&gt; {
  get(key: string): T
}

const FakeSmart = Stupid as new &lt;T&gt;() =&gt; ISmart&lt;T&gt;;

class Smart&lt;T&gt; extends FakeSmart&lt;T&gt; {
  get(key: string): T {
    return super.get(key);
  }
}

What we're doing is pretending that Stupid behaves like Smart. First we create an ISmart&lt;T&gt; interface which is the same as Stupid except that the generic is moved to where you want it. Then we assign the Stupid constructor to a new variable called FakeSmart and assert that FakeSmart has the type of a generic constructor. So FakeSmart is just Stupid at runtime, but the compiler thinks it is of a proper superclass of Smart&lt;T&gt;.

And then we declare that Smart&lt;T&gt; extends FakeSmart&lt;T&gt;. At runtime this is that same as your original code, but now at compile time there are no errors because the get() method of Smart&lt;T&gt; is compatible with the get() method of FakeSmart&lt;T&gt;. Note that the call super.get(key) doesn't take a type argument because FakeSmart.get() doesn't take a type argument.


So there you go. Personally I'd be worried about code like this where you can tell the compiler simultaneous contradictory things about the types, but if you're not the one writing Stupid then I guess the best you can do is wrap it in something more reasonable.

Playground link to code

huangapple
  • 本文由 发表于 2023年4月1日 00:33:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/75900825.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定