如何在TypeScript中声明逆变的类函数成员?
我有一个包含函数成员的类,该函数接收类的实例:
class Super { public member: (x: Super) => void = function(){} use() {const f = this.member; f(this)} }
但我希望member支持逆变特性;具体来说,我希望子类实例可以接收以该子类自身为参数的函数作为member的值,例如:
class Sub extends Super { method() {} } const sub = new Sub(); sub.member = function(x: Sub) {x.method()};
但tsc会正确报错:
Type '(x: Sub) => void' is not assignable to type '(x: Super) => void'.
Types of parameters 'x' and 'x' are incompatible.
Property 'method' is missing in type 'Super' but required in type 'Sub'.
如何声明member,才能让子类可以接收参数类型为自身的函数?
- 我知道用方法语法声明
member(member(s: Super) {/* ... */})会是双变的,但这不适用于member是函数字典的场景(比如实际代码中member类型是{[name: string]: (/*...*/, s: Super) => /*...*/})。 - 我尝试在
Sub中重新声明member为更严格的签名:
但tsc直接阻止了该操作:class Sub extends Super { public member: (x: Sub) => void = function(x){x.method()}; method() {} }Property 'member' in type 'Sub' is not assignable to the same property in base type 'Super'.
Type '(x: Sub) => void' is not assignable to type '(x: Super) => void'.
Types of parameters 'x' and 'x' are incompatible.
Property 'method' is missing in type 'Super' but required in type 'Sub'. - 我了解TypeScript现在支持在泛型上用
in和out修饰符表示协变/逆变,但不确定是否适用,也不知道怎么把Super改成合适的泛型声明。 - 我不想关闭
strictfunctionTypes,因为它通常很有用,也不想强迫库用户关闭这个选项来给子类实例的.member赋值。 - 作为最后手段,我可以把赋值的值强制转换为
as (x: Super) => void,但这样会失去错误赋值的保护,比如:
这会被tsc接受,但运行时会失败。class Sub1 extends Super { method1() {} } class Sub2 extends Super { method2() {} } const sub1 = new Sub1(); sub1.member = function(x: Sub2) {x.method2()} as (x: Super) => void; - 查看过相关问题,看到一个涉及接口的类似问题,但没有正式答案,也完全看不懂评论里链接的代码片段;它们似乎需要枚举所有子类型,不适合我这种可能有任意多子类的场景。
方案1:自引用泛型类(基础场景)
将Super声明为自引用泛型,让member的参数类型绑定到当前子类的类型,完美适配逆变需求:
class Super<T extends Super<T>> { public member: (x: T) => void = function() {}; use() { const f = this.member; f(this as unknown as T); } } class Sub extends Super<Sub> { method() {} } const sub = new Sub(); // 现在可以正常赋值,TS会自动检查参数类型 sub.member = function(x: Sub) { x.method(); }; // 错误赋值会被TS拦截,保证类型安全 class Sub1 extends Super<Sub1> { method1() {} } class Sub2 extends Super<Sub2> { method2() {} } const sub1 = new Sub1(); // 下面这行代码会触发TS错误,符合预期 // sub1.member = function(x: Sub2) { x.method2(); };
核心逻辑是通过泛型T让父类的member参数类型与子类自身关联,use方法中的this as unknown as T是安全的类型断言,因为运行时this必然是子类实例。
方案2:泛型接口+自引用泛型(函数字典场景)
如果你的实际场景是函数字典,可以用泛型接口定义member类型,确保每个函数的参数都是当前子类类型:
interface MemberDict<T> { [name: string]: (arg: any, x: T) => void; } class Super<T extends Super<T>> { public member: MemberDict<T> = {}; use(key: string) { const f = this.member[key]; if (f) { f(null, this as unknown as T); } } } class Sub extends Super<Sub> { method() {} } const sub = new Sub(); sub.member['test'] = function(_arg, x: Sub) { x.method(); }; // 错误赋值会被TS拦截 // sub.member['test'] = function(_arg, x: Sub2) { x.method2(); };
该方案同样利用自引用泛型绑定子类类型,既满足函数字典的需求,又保留了完整的类型检查能力。
内容的提问来源于stack exchange,提问作者cpcallen

