TypeScript如何类型安全实现属性取值Getter类
实现方案
这个需求可以在TypeScript中原生实现,核心是通过自引用映射类型解决属性值的toString安全约束,配合精确类型校验阻止多余属性、非法子类型绕过检查,全程是编译期类型校验,无任何运行时开销,完全适配构造层高开销、循环高频调用的业务场景。
正确实现代码
interface Stringable { toString: () => string } interface A { a: number } interface B { b: Date } // 工具类型:校验类型与允许的联合类型完全匹配,禁止额外属性 type Exactly<T, AllowedTypes> = AllowedTypes extends unknown ? (<V>() => V extends T ? 1 : 2) extends (<V>() => V extends AllowedTypes ? 1 : 2) ? T : never : never; class Getter<AB extends Exactly<AB, A | B> & { [K in keyof AB]: Stringable }> { key: keyof AB; constructor(key: keyof AB) { this.key = key; // 构造阶段高开销计算逻辑可直接写在这里,不受类型校验影响 } get(thing: AB): string { return thing[this.key].toString(); } }
效果验证
所有预期的类型校验规则都会正常触发:
- 合法场景正常通过:
const bGetter = new Getter<B>('b'); bGetter.get({b: new Date(0,0)}); // 正常返回字符串结果 - 传入目标类型不存在的key直接报错:
const aGetter = new Getter<A>('b'); // 编译报错:类型"b"不能赋给类型"a" - 传入不匹配目标类型的对象直接报错:
bGetter.get({a: 105}); // 编译报错:类型"{a: number}"缺少B类型必须的属性"b" - 带非法属性的子类型会被拦截:
interface C extends A { c: undefined } const cGetter = new Getter<C>('a'); // 编译报错:C带额外属性c,且c为undefined无toString方法,不满足约束
之前方案失败的原因
AB extends A | B方案缺陷:
TypeScript采用结构化类型系统,只要类型满足接口的最小结构要求就会通过校验,既允许子类型新增无toString方法的属性(比如undefined类型属性,调用toString会直接抛错),也允许传入带多余属性的结构,无法保证类型安全。AB extends Record<string, Stringable>方案缺陷:Record<string, Stringable>是带字符串索引签名的字典类型,要求对象支持任意字符串键的访问,而普通业务接口A/B只定义了有限的已知属性,没有索引签名,二者类型不兼容。
实现中使用的自引用映射类型{ [K in keyof AB]: Stringable }只会遍历AB自身的已知属性做校验,不需要索引签名,完美适配普通接口。
扩展说明
如果后续需要支持更多业务接口,只需要修改Exactly第二个参数的联合类型即可,比如新增interface C { c: boolean },就把联合类型改成A | B | C,不需要修改其他逻辑。
如果不需要严格限制只能传入预定义的接口类型,只需要保证所有属性有toString方法,可以去掉Exactly约束,只保留自引用的Stringable约束即可,允许传入任意满足属性可toString的类型。
内容的提问来源于stack exchange,提问作者Joel Newman
相关产品推荐
相关产品推荐

