如何为支持任意属性的JavaScript Proxy编写精确TypeScript类型?
问题描述
我需要为支持任意属性访问的JavaScript Proxy编写TypeScript类型。举个简化示例:一个能将每个属性映射为自身属性名的对象:
const proxy = new Proxy({}, { get(target, property) { return property; } }); proxy.foo // 实际求值为"foo" // 但TypeScript报错:Property 'foo' does not exist on type '{}'.ts(2339)
我不指望TypeScript自动推断proxy.foo存在,但希望通过类型定义让它知晓任意属性都存在,且属性值的类型与属性名完全一致。
用普通的映射类型虽然能消除属性不存在的报错,但会把值类型推断为string而非更精确的"foo":
const proxy = new Proxy<{ [T in string]: T }>({}, { get(target, prop) { return prop; } }); proxy.foo // 推断类型为string,而非预期的"foo"
我也试过{ readonly [T in string]: T },有意思的是,({} as { [T in "foo" | "bar"]: T }).foo会被正确推断为"foo",但我没办法枚举所有可能的键。
有没有办法让TypeScript在映射类型中把精确键名作为值类型,或者通过其他方式为带动态属性的Proxy编写精确类型?
解决方案
方案1:泛型索引类型结合类型断言
定义包含泛型索引签名的类型,通过类型断言为Proxy实例绑定该类型,TypeScript会在访问具体属性时自动推导键名的字面量类型:
type SelfReferencingProxy = { [K: string]: K; }; const proxy = new Proxy({}, { get(target, prop) { return prop; } }) as SelfReferencingProxy; const foo = proxy.foo; // 类型推断为 "foo" const customKey = proxy['user-name_123']; // 类型推断为 "user-name_123"
方案2:封装为泛型创建函数
把Proxy的创建逻辑封装成函数,返回值直接绑定目标类型,代码结构更清晰:
type SelfProxy = { [K: string]: K }; function createSelfProxy(): SelfProxy { return new Proxy({}, { get(target, prop) { return prop; } }); } const proxy = createSelfProxy(); const bar = proxy.bar; // 类型推断为 "bar"
原理说明
TypeScript默认处理索引签名时,会将值类型推断为键类型的联合(比如string),但当你将Proxy断言为{ [K: string]: K }这类泛型索引类型后,TypeScript会利用上下文类型推断能力,在访问具体属性时把当前键名的字面量类型代入到索引签名的K中,从而得到与键名完全一致的值类型。
对于已知枚举键的场景,TypeScript能直接匹配键与值的类型;而对于任意键的动态场景,通过上述方式让TypeScript在属性访问时动态推导,就能实现精确的类型约束。
内容的提问来源于stack exchange,提问作者tlrobinson

