如何在TypeScript键重映射映射对象中兼容forEach并保留类型安全?
用映射对象替代switch语句的类型优化方案
我常使用映射对象替代switch语句,以此提升代码的可读性与可扩展性,基础示例如下:
interface Foo { type: 'foo'; foo: number; } interface Bar { type: 'bar'; bar: string; } type FooBar = Foo | Bar; const mapper: Record<FooBar['type'], () => string | number> = { foo: () => 1, bar: () => 'bar', } const foo = { type: 'foo', foo: 1 } satisfies Foo; const bar = { type: 'bar', bar: '123' } satisfies Bar; mapper[foo.type]() // -> 1 mapper[bar.type]() // -> 'bar'
Record方案的弊端
这种基于Record的方案在多数场景下可行,但当需要向映射函数传递对象时存在明显问题:映射函数的参数会被统一类型化为联合类型FooBar,导致内部必须手动添加类型断言才能访问具体类型的属性。
// 沿用之前的Foo、Bar和FooBar类型 const mapper: Record<FooBar['type'], (fooBar: FooBar) => string | number> = { // 必须手动断言参数类型才能访问foo/bar属性 foo: (fooBar) => (fooBar as Foo).foo, bar: (fooBar) => (fooBar as Bar).bar, } const foo = { type: 'foo', foo: 1 } satisfies Foo; const bar = { type: 'bar', bar: '123' } satisfies Bar; mapper[foo.type](foo) // -> 1 mapper[bar.type](bar) // -> 'bar' // 此代码可运行,但类型检查不严谨(比如传入错误类型也不会报错) [foo, bar].forEach(fooOrBar => { mapper[fooOrBar.type](fooOrBar) })
键重映射的类型优化与遍历问题
使用TypeScript的键重映射可以优化类型定义,让每个映射函数的参数自动匹配对应类型,无需手动断言,但此时联合类型数组的forEach遍历会失效——因为TypeScript无法推断出遍历项的具体类型与mapper函数参数的对应关系,会出现类型缩减为never的错误。
// 沿用之前的Foo、Bar和FooBar类型 type Mapper = { [E in FooBar as E['type']]: (obj: E) => number | string; }; const mapper: Mapper = { foo: foo => foo.foo, bar: bar => bar.bar, }; const foo = { type: 'foo', foo: 1 } satisfies Foo; const bar = { type: 'bar', bar: 'bar' } satisfies Bar; mapper[foo.type](foo); // -> 1 mapper[bar.type](bar); // -> 'bar' // 无法运行,类型检查报错:Foo & Bar 缩减为 never [foo, bar].forEach(fooOrBar => { mapper[fooOrBar.type](fooOrBar) })
解决方案:保留键重映射类型同时支持遍历
可以通过泛型函数来关联遍历项的具体类型与映射函数的参数类型,既保留键重映射带来的精确类型约束,又让forEach遍历正常运行:
// 沿用之前的Foo、Bar、FooBar和Mapper定义 // 定义泛型处理函数,捕获每个遍历项的具体类型 const handleFooBar = <T extends FooBar>(obj: T) => { return mapper[obj.type](obj); }; // 现在forEach可以正常运行,且类型完全安全 [foo, bar].forEach(handleFooBar);
原理说明
泛型T extends FooBar会在每次调用时自动捕获当前遍历项的具体类型(Foo或Bar),TypeScript可以据此推断出mapper[obj.type]对应的函数参数类型正好是T,从而完成类型匹配,无需任何类型断言,同时保证了类型安全性。
内容的提问来源于stack exchange,提问作者Lehks
相关产品推荐
相关产品推荐

