如何在TypeScript中使用__proto__实现对象扩展?
我写了下面这段无法编译的TypeScript代码:
export function extendObject< T extends Object, X extends Object, >(x: T, a: X): T & X { const p = { __proto__: x } Object.assign(p, a) return p }
在return p处出现了如下编译错误:
Type '{ __proto__: T; }' is not assignable to type 'T & X'. Type '{ __proto__: T; }' is not assignable to type 'T'. '{ __proto__: T; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Object'.ts(2322)
这段代码的JavaScript版本是可以正常运行的:
export function extendObject(x, a) { const p = { __proto__: x } Object.assign(p, a) return p }
使用方式如下:
const a = { foo: 'bar' } const b = extendObject(a, { one: 2 }) assert(b.foo === 'bar') assert(b.one === 2)
我希望通过__proto__实现性能优化,请问怎么给这段代码添加正确的TypeScript类型注解来通过编译?如果不能用__proto__,还有哪些替代方案?
另外我还尝试了下面的写法,同样无法编译:
export function extendObject< T extends Object, X extends Object, >(x: T, a: X): T & X { const p: T & X = {} Object.keys(x).forEach(key => (p[key] = x[key])) Object.keys(a).forEach(key => (p[key] = a[key])) // Object.assign(p, a) return p }
一、修复使用__proto__的TypeScript版本
TypeScript报错的核心原因是:{ __proto__: T }无法被TS识别为严格的T类型——T可以是Object的任意子类型,仅设置原型并不代表对象本身符合T的结构约束。
我们可以通过类型断言告知TS最终的p符合T & X类型,同时建议把泛型约束从大写Object改为小写object(TS中object专门表示非原始值类型,比Object更精准):
export function extendObject< T extends object, X extends object, >(x: T, a: X): T & X { const p = { __proto__: x } as T & X; Object.assign(p, a); return p; }
或者在return时做断言,逻辑更清晰:
export function extendObject< T extends object, X extends object, >(x: T, a: X): T & X { const p = { __proto__: x }; Object.assign(p, a); return p as T & X; }
这样就能通过编译,同时保留原型链优化的逻辑——原型链继承无需复制所有属性,访问x的属性时通过原型查找,性能优于浅拷贝。
二、无法使用__proto__时的替代方案
1. 使用Object.create创建原型继承对象
这是__proto__的标准替代写法,TS对它的类型支持更友好,且是ES5标准API,兼容性更好:
export function extendObject< T extends object, X extends object, >(x: T, a: X): T & X { const p = Object.create(x); Object.assign(p, a); return p as T & X; }
它的作用和{ __proto__: x }完全一致,都是创建以x为原型的新对象。
2. 浅拷贝合并(无原型继承)
如果不需要原型链优化,仅需合并属性,可以直接用浅拷贝:
export function extendObject< T extends object, X extends object, >(x: T, a: X): T & X { return { ...x, ...a }; }
或者用Object.assign:
export function extendObject< T extends object, X extends object, >(x: T, a: X): T & X { return Object.assign({}, x, a); }
这种方式会把x和a的所有可枚举属性复制到新对象,无原型链依赖,但如果x属性较多,性能不如原型继承。
3. 修复遍历拷贝写法
你之前尝试的遍历写法报错,是因为{}无法直接断言为T & X,且Object.keys返回string[],TS无法确定key属于T或X。可以修改为:
export function extendObject< T extends object, X extends object, >(x: T, a: X): T & X { const p = {} as T & X; // 用类型断言处理x的key (Object.keys(x) as Array<keyof T>).forEach(key => { p[key] = x[key]; }); // 处理a的key (Object.keys(a) as Array<keyof X>).forEach(key => { p[key] = a[key]; }); return p; }
不过这种写法本质还是浅拷贝,和Object.assign/扩展运算符效果一致,没有原型链优化的优势。
内容的提问来源于stack exchange,提问作者Lance Pollard

