TypeScript类为何无法识别自身属性?泛型方法类型疑问
我们希望在父类中定义一个泛型方法,该方法接受子类中任意字符串类型属性的名称作为参数,简化示例代码如下:
class CanUpcase { upcase<PropertyName extends keyof this>( propertyName: this[PropertyName] extends string ? PropertyName : never ) { return this[propertyName].toUpperCase(); // Property 'toUpperCase' does not exist on type 'this[this[PropertyName] extends string ? PropertyName : never]'. } } class User extends CanUpcase { age = NaN; name = ``; constructor() { super(); this.upcase('name'); // Argument of type '"name"' is not assignable to parameter of type 'this["name"] extends string ? "name" : never'.ts(2345) } } const user = new User(); user.upcase('name'); // No error
预期upcase方法应当接受'name'作为参数(因为其属性类型是字符串),但实际出现两个异常情况:
- 实例调用
user.upcase('name')正常,构造函数内调用this.upcase('name')报错 upcase方法内部this[propertyName]未被识别为字符串类型
我们有两个疑问:
- 为何构造函数中的
this.upcase会报错,而实例调用user.upcase却不会? - 为何
this[propertyName]不被识别为字符串类型?
问题1:构造函数与实例调用的差异原因
在子类的构造函数中,this的类型是还没完全初始化的User类型——TypeScript为了避免你在构造函数里访问未初始化的属性,会把此时的this视为User的"半成品"版本,属性类型还处于可变状态(比如你可能在构造函数后续代码里修改name的类型)。
而创建user实例后,user的类型是完全初始化后的User类型,TypeScript能确定name的类型固定是字符串,所以能正确匹配泛型约束。
说白了就是:构造函数里的this是"未完工"的类型,实例是"完工"的类型,类型检查的信息完整性和严格程度不一样。
问题2:方法内部类型未被识别的原因
你写的条件类型this[PropertyName] extends string ? PropertyName : never只是用来过滤合法的参数属性名,但TypeScript没办法反向推导this[propertyName]的类型一定是字符串——因为这个条件类型要到方法调用时才会具体解析,方法内部没法提前确定this[propertyName]的具体类型。
要解决这个问题,得把泛型约束和类型关联做的更明确,比如直接限定属性值为字符串类型:
class CanUpcase { upcase<PropertyName extends keyof this>( propertyName: PropertyName & (this[PropertyName] extends string ? unknown : never) ): string { return (this[propertyName] as string).toUpperCase(); } }
或者换一种泛型定义方式,直接把返回值和属性类型绑定:
class CanUpcase { upcase<Prop extends keyof this>(prop: Prop): this[Prop] extends string ? string : never { return (this[prop] as string).toUpperCase() as any; } }
这样TypeScript就能明确知道,传入合法参数时返回值是字符串,方法内部也可以通过类型断言绕过暂时的类型检查(或者通过更精确的泛型约束让TypeScript自动推导)。
内容的提问来源于stack exchange,提问作者RobertAKARobin

