如何为包装对象的JavaScript类编写TypeScript类型声明
问题
我有一个用于包装对象并提供额外属性的JavaScript类ApiObj,希望为其编写TypeScript类型声明,以在类型中体现该包装功能。
简化示例类代码:
// apiObj.js class ApiObj { constructor(obj) { for (const [key, val] of Object.entries(obj)) { this[key] = val } } get myAdditionalFeature() { return "this is an example" } }
使用方式:
const myObj = { a: "test" } const apiWrapper = new ApiObj(myObj) const valA = apiWrapper.a // "test" const valAdditionalFeature = apiWrapper.myAdditionalFeature // "this is an example"
apiWrapper同时具备原对象的属性a和额外属性myAdditionalFeature。
我尝试了两种方法但均存在问题:
- 方法1:让
ApiObj<T>继承T
// apiObj.d.ts declare class ApiObj<T> extends T { // 无法识别属性a,仅能识别ApiObj新增的属性 constructor(obj: T) get myAdditionalFeature(): string }
问题:TypeScript不允许类泛型继承泛型参数T(除非T是具体类类型),接口或对象类型不能作为类的基类,导致无法正确推断原对象属性。
- 方法2:使用类型交叉别名
// apiObj.d.ts declare class ApiObj<T> { constructor(obj: T) get myAdditionalFeature(): string } type ApiObj<T> = ApiObj<T> & T // 类型交叉,看似能生成正确类型
问题:同名的类声明和类型别名会冲突,TypeScript优先使用类类型,导致无法访问原对象的属性。
临时解决方案是通过工厂函数实现,但只能通过工厂函数实例化对象,无法为ApiObj的构造函数实现对应类型:
// apiObj.d.ts declare class $ApiObj { get myAdditionalFeature(): string } type ApiObj<T> = $ApiObj & T declare function apiObj<T extends Object>(obj: T): ApiObj<T>
使用方式:
// usage.ts interface Test { a: string } const myTest: Test = { a: "Test" } const t = apiObj(myTest) t.a // 正常 t.myAdditionalFeature // 正常
请问该功能能否通过TypeScript实现?若可以,正确的实现方式是什么?
解决方案
可以实现,核心思路是利用构造签名的类型交叉,让ApiObj的构造函数返回交叉类型,同时避免类声明与类型别名的名称冲突。
正确的类型声明方式
// apiObj.d.ts // 声明ApiObj的基础类,仅包含额外属性 declare class ApiObjBase { get myAdditionalFeature(): string; } // 定义构造函数类型:接收T类型参数,返回ApiObjBase与T的交叉类型 type ApiObjConstructor = { new <T extends object>(obj: T): ApiObjBase & T; }; // 将全局的ApiObj指向这个构造函数类型 declare const ApiObj: ApiObjConstructor;
使用验证
// usage.ts interface Test { a: string; } const myTest: Test = { a: "Test" }; const myApi = new ApiObj(myTest); myApi.a; // 类型正确,string myApi.myAdditionalFeature; // 类型正确,string
原理说明
- 用
ApiObjBase单独定义类的固有属性,避免与交叉类型的名称冲突; - 通过构造签名
new <T>(obj: T): ApiObjBase & T,告知TypeScript:调用new ApiObj(obj)时,返回的实例同时拥有obj的所有属性和ApiObjBase的属性; - 这种方式完全匹配JavaScript类的运行时行为,既保留了
new关键字实例化的方式,又能正确推断原对象的属性类型。
简化写法
如果不想单独声明ApiObjBase,也可以直接在构造签名中定义额外属性:
// apiObj.d.ts type ApiObjExtra = { get myAdditionalFeature(): string; }; declare const ApiObj: { new <T extends object>(obj: T): T & ApiObjExtra; };
使用效果与上述方案完全一致,代码更简洁。
内容的提问来源于stack exchange,提问作者Tobias Braun
相关产品推荐
相关产品推荐

