游乐场链接。 我有一个对象类型的可区分联合:
export type ADiscriminatedUnion = {
type: 'a',
dataKeyA: number,
anotherDataKeyA: number,
} | {
type: 'b',
dataKeyB: number,
anotherDataKeyB: number,
} | {
type: 'c',
}
type AllTypes = ADiscriminatedUnion['type'];
我创建一个对象,它将 some 类型(但不是全部)映射到该类型对象中存在的部分属性列表:
const DiscriminatedUnionTypeToListOfDataKeysMap = {
a: ['dataKeyA'],
b: ['dataKeyB', 'anotherDataKeyB'],
} as const satisfies {
[Type in AllTypes]?: Array<
keyof Omit<Extract<ADiscriminatedUnion, { type: Type }>, 'type'>
>;
};
type TypesWhichAreMappedToDataKeys =
keyof typeof DiscriminatedUnionTypeToListOfDataKeysMap;
现在我想创建一个函数,它只能采用第二个代码块的映射中定义的类型的对象,并通过列出的属性进行映射。
function doSomethingWithDataKeys(
data: Extract<ADiscriminatedUnion, { type: TypesWhichAreMappedToDataKeys }>
) {
const stringifiedDataKeys = DiscriminatedUnionTypeToListOfDataKeysMap[data.type]
.map(key => `${data[key]}`).join('_');
}
此错误在
data[key]
:
Property 'dataKeyA' does not exist on type '{ type: "a"; dataKeyA: number; anotherDataKeyA: number; } | { type: "b"; dataKeyB: number; anotherDataKeyB: number; }'.
TypeScript 似乎不会缩小
DiscriminatedUnionTypeToListOfDataKeysMap[data.type]
的范围以将结果值(属性名称数组)与传递的对象的属性相匹配。我如何指示它这样做?
您遇到了 microsoft/TypeScript#30581 的变体。当
data.type
属于 union 类型时,TypeScript 无法理解
data[key]
和 data
之间的相关性。它需要“立即”这样做,它不会预先将 data
缩小到 ADiscriminatedUnion
的每个工会成员并分别检查每个缩小范围。对于在一个地方发生两个成员的工会来说这没什么问题,但这种事情无法规模化。
让 TypeScript 立即分析通用内容的方法是使用 generics。对于相关联合,microsoft/TypeScript#47109 中描述了一种特殊的重构方式,它是有效的。该方法是用某种“基本”键值类型、或该类型上的“映射类型”、或这些类型的通用“索引”来表示您正在做的所有事情。
对于您的示例,最干净的重构如下。首先,让我们定义基本键值类型:
interface BaseType {
a: {
dataKeyA: number;
anotherDataKeyA: number;
};
b: {
dataKeyB: number;
anotherDataKeyB: number;
};
c: {
dataKeyC: number;
anotherDataKeyC: number;
};
}
歧视联合
可以在BaseType
中变得通用,特别是,将是一个分布式对象类型,如ms/TS#47109中创造的:
type ADiscriminatedUnion<K extends keyof BaseType = keyof BaseType> =
{ [P in K]: { type: P } & BaseType[P] }[K]
名为 ADiscriminatedUnion
且没有类型参数的类型(使用 keyof BaseType
的 默认类型参数
)与您的版本等效,但它的表示方式使其余代码对编译器来说更容易关注:
export const DiscriminatedUnionTypeToListOfDataKeysMap = {
a: ['dataKeyA'],
b: ['dataKeyB', 'anotherDataKeyB'],
} as const satisfies { [K in keyof BaseType]?: (keyof BaseType[K])[] }
type TypesWhichAreMappedToDataKeys =
keyof typeof DiscriminatedUnionTypeToListOfDataKeysMap;
function doSomethingWithDataKeys<K extends TypesWhichAreMappedToDataKeys>(
data: ADiscriminatedUnion<K>
) {
const d: { [K in TypesWhichAreMappedToDataKeys]: (keyof BaseType[K])[] } =
DiscriminatedUnionTypeToListOfDataKeysMap;
const stringifiedDataKeys = d[data.type]
.map(key => `${data[key]}`).join('_');
}
我必须将
DiscriminatedUnionTypeToListOfDataKeysMap
扩展为 { [K in TypesWhichAreMappedToDataKeys]: (keyof BaseType[K])[] }
类型,这让编译器能够理解 DiscriminatedUnionTypeToListOfDataKeysMap[data.type]
属于
(keyof BaseType[K])[]
类型,这意味着
key
属于
keyof BaseType[K]
类型,这是可行的,因为
data
可分配给
BaseType[K]
,因此
data[key]
是
BaseType[K][keyof BaseType[K]]
,它是所有
BaseType[K]
值类型的并集。由于所有这些都是可序列化的
number
,因此代码可以编译。请注意,您
可以从原始的
ADiscriminatedUnion
计算BaseType
,但这更复杂:
type BaseType = { [T in ADiscriminatedUnion as T['type']]:
{ [K in keyof T as K extends "type" ? never : K]: T[K] }
};
然后您需要使用
ADiscriminatedUnion
的分布式对象类型版本而不是原始版本,以便 TypeScript 遵循相关性:type DiscU<K extends keyof BaseType = keyof BaseType> =
{ [P in K]: { type: P } & BaseType[P] }[K]
如果您无法控制原始的
ADiscriminatedUnion
类型,我只会走这条路。但无论哪种方式都有效。Playground 代码链接