我可以让 Typescript 根据可区分的联合键缩小对象值类型吗?

问题描述 投票:0回答:1

游乐场链接。 我有一个对象类型的可区分联合:

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]
的范围以将结果值(属性名称数组)与传递的对象的属性相匹配。我如何指示它这样做?

typescript discriminated-union
1个回答
0
投票

您遇到了 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 代码链接


    

© www.soinside.com 2019 - 2024. All rights reserved.