抽象泛型函数的打字稿模式似乎被打破了
Typescript pattern for abstract generic functions seems to be broken
我有一个这样的模式,我在几个月(甚至几年)前使用旧版本的打字稿编译器编写的代码中使用了很多,可能是 1.8 - 2.2:
interface IBase { };
interface IExtends extends IBase {
key2: string;
};
abstract class MyAbstractClass {
protected abstract myFunc<T extends IBase>(arg: string): T;
};
class MyClass extends MyAbstractClass {
protected myFunc(arg: string): IExtends {
return { key2: arg };
}
};
回到我写这段代码的时候,Typescript 根本没有抱怨,并且按照你期望的方式处理了它。
但是现在,typescript(版本 2.8.1)抱怨:
src/errorInvestigation.ts(12,15): error TS2416: Property 'myFunc' in type 'MyClass' is not assignable to the same property in base type 'MyAbstractClass'.
Type '(arg: string) => IExtends' is not assignable to type '<T extends IBase>(arg: string) => T'.
Type 'IExtends' is not assignable to type 'T'.
如果 typescript 将其标记为错误是正确的,那么完成同样事情的正确方法是什么?
非常感谢。
我认为您可能并不是真的想让 myFunc
通用。以下签名
abstract class MyAbstractClass {
protected abstract myFunc<T extends IBase>(arg: string): T;
};
表示 MyAbstractClass
有一个 myFunc()
方法,该方法采用 string
输入和 return 任何 IBase
的子类型,来电者指定。但是您希望它成为 return IBase
的子类型,由 实现者 指定。如果 myFunc
不是 protected
,您可以像
一样快速发现问题
declare const myAbstractInstance: MyAbstractClass;
let iBase = myAbstractInstance.myFunc<IBase>('hey'); // IBase
let iExtends = myAbstractInstance.myFunc<IExtends>('hey'); // IExtends?!
let iOther = myAbstractInstance.myFunc<IBase & {foo: string}>('hey'); // What?!
iOther = iExtends; // error, not assignable
你用相同的参数调用同一个函数三次,在运行时它们会有相同的输出,但显然 TypeScript 认为输出的类型不同。这是一个奇怪的模式,而不是您打算传达的内容。
而且我不确定它是什么时候强制执行的,但是你不应该被允许用 non-generic 覆盖泛型函数,除非 non-generic 确实是通用的。但是由于 T
可以是调用者指定的 IBase
的 any 子类型,因此唯一可以明确分配给每个可能的 [=22] 的 non-generic 类型=] 将是 never
和 any
。我想你不想 return 那些。
听起来你只是想要 myFunc
到 return 一个 IBase
或子类实现指定的它的某个子类型。如果是这样,那么您完全可以在没有泛型的情况下做到这一点:
abstract class MyAbstractClass {
protected abstract myFunc(arg: string): IBase;
};
这对你有用。子类
class MyClass extends MyAbstractClass {
protected myFunc(arg: string): IExtends {
return { key2: arg };
}
};
与 MyAbstractClass
兼容,因为方法 return 类型是 covariant,这意味着如果 Y
是 X
的子类型,则方法Y
允许 return X
的类似方法的子类型。
如果您需要方法 parameter 在子类中变窄,这会有所不同,因为方法参数是逆变的。在 T
是 arg
类型而不是 return 类型的情况下,上述不应该工作......但它 确实 碰巧起作用是因为方法参数仍然被认为是 bivariant。但我认为这不是您的用例。 (更新:我看到它 是 你的用例。显然由于这个原因,方法参数是双变的,所以试试看。我个人不喜欢双变,还有更多 "correct" 解决方法,但它们可能比它们的价值更麻烦。)
希望对您有所帮助;祝你好运。
非常感谢 jcalz 让我走上了研究之路,得到了一个令人遗憾但可行的答案
对于打字稿为何进行此更改,我仍然感到困惑,因为我认为原始描述方式没有问题。
答案是将泛型提升到 class 级别,而不是将它们留在方法上。这有利有弊,在我看来利大于弊。优点是摘要的正文 class 可能更清晰、更易于阅读。缺点是,如果您有许多不同的类型需要在同一个 class 中处理,那么 class 本身需要在 class 声明和每个派生的 [=] 上声明所有这些类型22=] 也需要这样做,这意味着我认为这更难阅读。但它会完成工作。
这是我看到的完整答案,我希望任何人(包括 jcalz!)都能进一步补充 thoughts/refinements:
interface IArgBase {
baseArgKey: string;
};
interface IReturnBase {
baseReturnKey: string;
};
interface IPromiseBase {
basePromiseKey: string;
};
interface IArgExtends extends IArgBase {
extendedArgKey: string;
};
interface IReturnExtends extends IReturnBase {
extendedReturnKey: string;
};
interface IPromiseExtends extends IPromiseBase {
extendedPromiseKey: string;
};
abstract class MyAbstractClass<TArg extends IArgBase, TReturn extends IReturnBase, TPromise extends IPromiseBase>{
protected abstract myFunc(arg: TArg): TReturn;
protected abstract myAsyncFunc(arg: TArg): Promise<TPromise>;
};
class MyClass extends MyAbstractClass<IArgExtends, IReturnExtends, IPromiseExtends> {
protected myFunc(arg: IArgExtends): IReturnExtends {
return { baseReturnKey: arg.baseArgKey, extendedReturnKey: arg.extendedArgKey };
};
protected myAsyncFunc(arg: IArgExtends): Promise<IPromiseExtends> {
return Promise.resolve({ basePromiseKey: arg.baseArgKey, extendedPromiseKey: arg.extendedArgKey });
};
};
我有一个这样的模式,我在几个月(甚至几年)前使用旧版本的打字稿编译器编写的代码中使用了很多,可能是 1.8 - 2.2:
interface IBase { };
interface IExtends extends IBase {
key2: string;
};
abstract class MyAbstractClass {
protected abstract myFunc<T extends IBase>(arg: string): T;
};
class MyClass extends MyAbstractClass {
protected myFunc(arg: string): IExtends {
return { key2: arg };
}
};
回到我写这段代码的时候,Typescript 根本没有抱怨,并且按照你期望的方式处理了它。
但是现在,typescript(版本 2.8.1)抱怨:
src/errorInvestigation.ts(12,15): error TS2416: Property 'myFunc' in type 'MyClass' is not assignable to the same property in base type 'MyAbstractClass'.
Type '(arg: string) => IExtends' is not assignable to type '<T extends IBase>(arg: string) => T'.
Type 'IExtends' is not assignable to type 'T'.
如果 typescript 将其标记为错误是正确的,那么完成同样事情的正确方法是什么?
非常感谢。
我认为您可能并不是真的想让 myFunc
通用。以下签名
abstract class MyAbstractClass {
protected abstract myFunc<T extends IBase>(arg: string): T;
};
表示 MyAbstractClass
有一个 myFunc()
方法,该方法采用 string
输入和 return 任何 IBase
的子类型,来电者指定。但是您希望它成为 return IBase
的子类型,由 实现者 指定。如果 myFunc
不是 protected
,您可以像
declare const myAbstractInstance: MyAbstractClass;
let iBase = myAbstractInstance.myFunc<IBase>('hey'); // IBase
let iExtends = myAbstractInstance.myFunc<IExtends>('hey'); // IExtends?!
let iOther = myAbstractInstance.myFunc<IBase & {foo: string}>('hey'); // What?!
iOther = iExtends; // error, not assignable
你用相同的参数调用同一个函数三次,在运行时它们会有相同的输出,但显然 TypeScript 认为输出的类型不同。这是一个奇怪的模式,而不是您打算传达的内容。
而且我不确定它是什么时候强制执行的,但是你不应该被允许用 non-generic 覆盖泛型函数,除非 non-generic 确实是通用的。但是由于 T
可以是调用者指定的 IBase
的 any 子类型,因此唯一可以明确分配给每个可能的 [=22] 的 non-generic 类型=] 将是 never
和 any
。我想你不想 return 那些。
听起来你只是想要 myFunc
到 return 一个 IBase
或子类实现指定的它的某个子类型。如果是这样,那么您完全可以在没有泛型的情况下做到这一点:
abstract class MyAbstractClass {
protected abstract myFunc(arg: string): IBase;
};
这对你有用。子类
class MyClass extends MyAbstractClass {
protected myFunc(arg: string): IExtends {
return { key2: arg };
}
};
与 MyAbstractClass
兼容,因为方法 return 类型是 covariant,这意味着如果 Y
是 X
的子类型,则方法Y
允许 return X
的类似方法的子类型。
如果您需要方法 parameter 在子类中变窄,这会有所不同,因为方法参数是逆变的。在 T
是 arg
类型而不是 return 类型的情况下,上述不应该工作......但它 确实 碰巧起作用是因为方法参数仍然被认为是 bivariant。但我认为这不是您的用例。 (更新:我看到它 是 你的用例。显然由于这个原因,方法参数是双变的,所以试试看。我个人不喜欢双变,还有更多 "correct" 解决方法,但它们可能比它们的价值更麻烦。)
希望对您有所帮助;祝你好运。
非常感谢 jcalz 让我走上了研究之路,得到了一个令人遗憾但可行的答案
对于打字稿为何进行此更改,我仍然感到困惑,因为我认为原始描述方式没有问题。
答案是将泛型提升到 class 级别,而不是将它们留在方法上。这有利有弊,在我看来利大于弊。优点是摘要的正文 class 可能更清晰、更易于阅读。缺点是,如果您有许多不同的类型需要在同一个 class 中处理,那么 class 本身需要在 class 声明和每个派生的 [=] 上声明所有这些类型22=] 也需要这样做,这意味着我认为这更难阅读。但它会完成工作。
这是我看到的完整答案,我希望任何人(包括 jcalz!)都能进一步补充 thoughts/refinements:
interface IArgBase {
baseArgKey: string;
};
interface IReturnBase {
baseReturnKey: string;
};
interface IPromiseBase {
basePromiseKey: string;
};
interface IArgExtends extends IArgBase {
extendedArgKey: string;
};
interface IReturnExtends extends IReturnBase {
extendedReturnKey: string;
};
interface IPromiseExtends extends IPromiseBase {
extendedPromiseKey: string;
};
abstract class MyAbstractClass<TArg extends IArgBase, TReturn extends IReturnBase, TPromise extends IPromiseBase>{
protected abstract myFunc(arg: TArg): TReturn;
protected abstract myAsyncFunc(arg: TArg): Promise<TPromise>;
};
class MyClass extends MyAbstractClass<IArgExtends, IReturnExtends, IPromiseExtends> {
protected myFunc(arg: IArgExtends): IReturnExtends {
return { baseReturnKey: arg.baseArgKey, extendedReturnKey: arg.extendedArgKey };
};
protected myAsyncFunc(arg: IArgExtends): Promise<IPromiseExtends> {
return Promise.resolve({ basePromiseKey: arg.baseArgKey, extendedPromiseKey: arg.extendedArgKey });
};
};