使用ES5与JSDoc构建类型安全类系统时,如何为构造函数实例类型添加属性并保留原类型参数?
我最近在折腾用ES5搭配JSDoc做一个类型安全的类系统,核心需求其实很直白:给构造函数返回的实例类型额外加个属性,同时还得完完整整保留原构造函数的类型参数。
先给你看看我写的cls包装函数,初衷就是给构造函数的实例添属性:
/** * @param {C} constructor * @template {new (...args: any) => {}} C * @returns {new (...args: ConstructorParameters<C>) => InstanceType<C> & { addedProp: "addedProp" }} */ function cls(constructor) { // 实现细节先放一边,重点看类型定义部分 return; }
用法大概是这样的,比如我定义一个带泛型的构造函数,用cls包一下:
var A = cls( /** * @constructor * @param {T} a * @template T */ function A(a) { this.a = a; } );
遇到的问题
结果踩了个坑:当构造函数本身带有泛型参数的时候,经过cls包装后,原有的泛型参数好像没被正确保留。就拿这个原构造函数来说:
/** * @constructor * @param {T} a * @template T */ function A(a) { this.a = a; }
它本身是带泛型T的——也就是创建实例时传入什么类型的a,实例的a属性就是对应类型。但用原来的cls包装后,返回的A构造函数好像丢失了这个泛型能力,实例的a属性类型没法正确推断了。
解决思路
后来我发现问题出在cls的泛型约束上。原来的@template {new (...args: any) => {}} C只约束了C是一个非泛型的构造函数,完全没考虑构造函数本身带泛型参数的情况。
那怎么调整呢?只需要修改cls的JSDoc泛型定义,让它能接受本身带有泛型参数的构造函数就行。比如把泛型约束改成支持带泛型的构造函数:
/** * @param {C} constructor * @template {new <T>(...args: any) => {}} C * @returns {new <T>(...args: ConstructorParameters<C>) => InstanceType<C> & { addedProp: "addedProp" }} */ function cls(constructor) { // 实现细节忽略 return; }
如果原构造函数有多个泛型参数,还可以写成<T, U>这种形式,灵活适配不同的构造函数。
验证效果
现在再用修改后的cls来定义A:
var A = cls( /** * @constructor * @param {T} a * @template T */ function A(a) { this.a = a; } ); // 这时候new A(123)的实例类型会被正确推断为:{ a: number, addedProp: "addedProp" } // new A('hello')的实例类型就是:{ a: string, addedProp: "addedProp" }
完美!原构造函数的泛型参数T被完整保留了,实例同时拥有原属性a和新增的addedProp,类型检查完全正常。
核心原因就是调整后的泛型约束能正确“识别”带泛型的构造函数,InstanceType<C>和ConstructorParameters<C>也能正确处理这类构造函数,最终返回的新构造函数自然就继承了原有的泛型能力,实例类型也实现了原类型与新增属性的组合。
备注:内容来源于stack exchange,提问作者Shmookoff

