以下TypeScrip中的示例代码片断使用mapper对象将数组arr的值映射到Mappings中包含的类型,该对象包含每个arr项的转换器函数:(Playground)

const arr = ["foo", "12", 42] as const;

type Mappings = { foo: boolean, "12": number, 42: string };

type MapperArgs<K extends (typeof arr)[number]> = {
    v: K,
    i: number
};
const mapper: {
    [K in (typeof arr)[number]]: (o: MapperArgs<K>) => Mappings[K]
} = {
    foo: ({ v, i }) => v.length + i > 4,
    "12": ({ v, i }) => Number(v) + i,
    42: ({ v, i }) => `${v}${i}`,
}

要在所有项目上运行mapper,通常的arr.map不会像预期的那样工作:

arr.map((v) => mapper[v]({ v })); /* TS error 2345: ... intersection was reduced to 'never' because ... has conflicting types in some constituents */

显然,这是因为TypeScripts limitations regarding correlated union types美元.可以使用通用函数来解决此问题:

const resolveMapper = <K extends keyof typeof mapper>(key: K, o: MapperArgs<K>) => mapper[key](o);
arr.map((v, i) => resolveMapper(v, { v, i })); // ok ✅

但是,在将某些映射设置为可选后,这将不起作用:

type SetOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// ...
const mapper: SetOptional<{
    [K in (typeof arr)[number]]: (o: MapperArgs<K>) => Mappings[K]
}, "foo"> = /* ... */

const resolveMapper = <K extends keyof typeof mapper>(key: K, o: MapperArgs<K>) => mapper[key]?.(o); /* not ok ❌ */

有没有办法在不 suppress 错误或使用类型断言/any的情况下解决这个问题?

推荐答案

microsoft/TypeScript#47109中描述的处理相关联合的方法(参见ms/TS#30581)仅在操作和类型采用特定形式时有效.你必须有一些"基本"对象类型,然后把东西写成generic,indexes into那个基本类型,或者写成mapped types,覆盖那个基本类型的属性.

你的Mappings是一个合理的基本对象类型,{ [K in (typeof arr)[number]]: (o: MapperArgs<K>) => Mappings[K] }keyof Mappings之上的映射类型(我不确定为什么你使用(typeof arr)[number]而不是keyof Mappings,但我不打算深入研究这一点……我将使用等价的类型{ [K in keyof Mappings]: (o: MapperArgs<K>) => Mappings[K] }.

但当您将其包装在SetOptional<⋯, "foo">中时,它不再是映射类型.它有intersection种映射类型,每一种类型都不同地处理不同的属性集.{ [K in keyof Mappings]: (o: MapperArgs<K>) => Mappings[K] }起作用的唯一原因是因为编译器可以"看到"对于泛型K,用K进行索引会给出单个泛型函数类型(o: MapperArgs<K>) => Mappings[K].在SetOptional<⋯, "foo">中try 这样做,你会得到一些奇怪的交集,编译器在不知道K是什么的情况下无法计算这些东西.


要做到这一点,最接近的方法是应用SetOptionalMappings来获得一个新的基类型,然后映射到该基类型上.它可能如下所示:

type PartMappings = SetOptional<Mappings, "foo">;

const mapper: {
    [K in keyof PartMappings]: (o: MapperArgs<K>) => PartMappings[K]
} = {
    foo: ({ v, i }) => v.length + i > 4,
    "12": ({ v, i }) => Number(v) + i,
    42: ({ v, i }) => `${v}${i}`,
}

const resolveMapper = <K extends keyof typeof mapper>(
    key: K, o: MapperArgs<K>) => mapper[key]?.(o)
//  const resolveMapper: <K extends "foo" | "12" | 42>(
//  key: K, o: MapperArgs<K>) => PartMappings[K]

现在编译器看到resolveMapper的返回类型是PartMappings[K],这是准确的.


注意,我说过这是你能得到的最接近的.它不是完全类型安全的.由于Watever的原因,(typeof Mapper)[K]不会被视为可能为空,因此您可以在函数中编写不正确的代码mapper[key](o)而不是mapper[key]?.(o),而不会得到错误.这很不幸,但这似乎是MS/TS#47109实现方式的一部分.人们可能会考虑提交一份关于它的错误报告,但即使确定它是一个错误,MS/TS#47109中的方法也不是灵丹妙药.一般来说,TypeScrip不是完全类型安全的,MS/TS#47109也不是特别安全;请参见microsoft/TypeScript#48730.

因此,当考虑是否对此代码进行重构时,无论是好是坏,问题并不是"这种重构是否使类型判断完全准确",而是"它是否使类型判断达到more accurate,而不是像type assertions这样的替代代码"(当然,也就是根据用例来权衡误报和漏报的风险).

Playground link to code

Typescript相关问答推荐

相同端点具有不同响应时的RTKQuery类型定义

TypScript手册中的never[]参数类型是什么意思?

用泛型类型覆盖父类函数导致类型y中的Property x不能赋给基类型Parent中的相同属性."'''''' "

如果存在ts—mock—imports,我们是否需要typescpt中的IoC容器

如何使用泛型类型访问对象

Angular 行为:属性类型从SignalFunction更改为布尔

如何提取密钥及其对应的属性类型,以供在新类型中使用?

如何在Typescript 中组合unions ?

为什么我的导航在使用Typescript的React Native中不起作用?

根据类型脚本中的泛型属性 Select 类型

为什么发送空字段?

作为函数参数的联合类型的键

在抽象类构造函数中获取子类型

在Google授权后由客户端接收令牌

为什么看起来相似的代码在打字时会产生不同的错误?

如何在TypeScript中描述和实现包含特定属性的函数?

React Context TypeScript错误:属性';firstName';在类型';iCards|iSearch';

带动态键的 TS 功能

TS2532:对象可能未定义

如何将参数传递给条件函数类型