You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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为更严格的签名:
    class Sub extends Super {
        public member: (x: Sub) => void = function(x){x.method()};
        method() {}
    }
    
    但tsc直接阻止了该操作:

    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,但这样会失去错误赋值的保护,比如:
    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;
    
    这会被tsc接受,但运行时会失败。
  • 查看过相关问题,看到一个涉及接口的类似问题,但没有正式答案,也完全看不懂评论里链接的代码片段;它们似乎需要枚举所有子类型,不适合我这种可能有任意多子类的场景。

解决方案

方案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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 06:49:57