如何从可区分的并集类型中选择键?



我有一个有区别的联合

type MyDUnion = { type: "anon"; name: string } | { type: "google"; idToken: string };

我想直接从MyDUnion类型中从判别性联合中访问名称键。像这样的东西

type Name = MyDUnion['name']

但是打字稿不允许这样做

Property 'name' doesn't exist on type '{ type: "anon"; name: string } | { type: "google"; idToken: string }'

如何访问它?

需要明确的是,这不是一个有效的解决方案:

type MyName = string;
type MyDUnion = { type: "anon"; name: MyName } | { type: "google"; idToken: string };
type Name = MyName; // this line would be in a different file

这是无效的,因为这样我将不得不导出MyNameMyDUnion类型以在其他地方使用。

有什么想法吗?

为了过滤对象的联合,通常需要使用 Extract: 简单的方法:

type Result = Extract<MyDUnion , {type: "anon"}>

更坚固:

type MyDUnion = { type: "anon"; name: string } | { type: "google"; idToken: string };
type Filter<Union, Type extends Partial<Union>> = Extract<Union, Type>
type Result = Filter<MyDUnion, { type: 'anon' }>

通用解决方案

/**
* @param Union - A discriminated union type to extract properties from.
* @param Keys - The specific properties to extract from `Union`.
* @defaultValue all `KeyOf<Union>`
* @param Otherwise - The type to unionize with value types that don't exist in all members of `Union`.
* @defaultValue `undefined`
*/
export type PickAll<
Union,
Keys extends KeyOf<Union> = KeyOf<Union>,
Otherwise = undefined
> = {
[_K in Keys]: Union extends { [K in _K]?: infer Value }
? UnionToIntersection<Value>
: Otherwise
}

助手

type KeyOf<Union, Otherwise = never> = Union extends Union
? keyof Union
: Otherwise
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
<小时 />

目标

type MyDUnion =
| { type: 'anon'; name: string }
| { type: 'google'; idToken: string }

indexed accesskeyof

MyDUnion['type']
/* "anon' | "google" */
// OK
MyDUnion[keyof MyDUnion]
/* "anon" | "google" */
// ❓
// @ts-expect-error - not all union members have an idToken
MyDUnion['type' | 'idToken']
/* any */
// ❌

KeyOf<Union>

type UnionKeys = KeyOf<MyDUnion>
/* "type" | "name" | "idToken" */
// ✅

PickAll<Union, KeyOf<Union>?, Otherwise?>

默认情况下,"全选">

type DUnionProps = PickAll<MyDUnion>
/* {
type: "anon" | "google";
name: string | undefined;
idToken: string | undefined;
} */
// ✅

专注于特定Key(+IDE 提示和类型检查)

ctrl+space是 OP

type DUnionName = PickAll<MyDUnion, 'name'>
/* { 
name: string | undefined
} */
// ✅

Keys的结合

type DesiredProps = PickAll<
MyDUnion | { fish: number },
'type' | 'idToken' | 'fish'
>
/* {
type: "anon" | "google" | undefined;
idToken: string | undefined;
fish: number | undefined;
} // ✅ */

陷阱

  • 不区分undefinedoptional属性。虽然可以做到,但它在待办事项上。

  • 直接提取文本。

别这样:

type应该"anon"|"google"

type GoogleLike = PickAll<MyDUnion, 'type' | 'name'>
type g = GoogleLike['name' | 'type']
/* string | undefined    */

这样做

type GoogleLikeTypes = GoogleLike['type']
/* "anon" | "google" */
// ✅
type GoogleLikeNames = GoogleLike['name']
/* string | undefined */
// ✅ (weird, be mindful)
<小时 />

编辑

我忘了提到第三个参数可用于更改回退类型。默认值是未定义的,我认为这是最安全的行为,但您也可以将其设置为您想要的任何内容。例如,PickAll<Union, Keys, never>等效于Required<PickAll<Union, Keys>>,或者至少如果Required可以推断出像PickAll这样的类型。

<小时 />

更新2

偶然发现了一个"现实"的KeyOf示例,我认为这将有助于阐明为什么它是解决方案的必要部分。

type Operations = {
a: { responses: ApiResponse[] }
b: { responses: ApiResponse[]; parameters: string[] }
}

使用内置keyof,将删除"parameters"属性,因此此代码将出错:

type PickInner<T, P extends keyof T[keyof T]> = {
[K in keyof T as T[K] extends { [K2 in P]: unknown } ? K : never]: T[K]
}
type q = PickInner<Operations, 'parameters'> 
// Type '"parameters"' does not satisfy the constraint '"responses"

但是我们可以将第一个keyof换成KeyOf<>,突然我们的实用程序起作用了:

- type PickInner<T, P extends keyof T[keyof T]> = {
+ type PickInner<T, P extends KeyOf<T[keyof T]>> = {
type q = PickInner<Operations, 'parameters'>
/* {
b: {
responses: ApiResponse[];
parameters: string[];
};
} */

为了获得所需的密钥,您必须以某种方式告诉 Typescript 编译器

嘿编译器,我想知道这个对象,在一个非常特殊的情况下

在这种情况下,您想知道对象,当type ==='anon'.所以,以你为例,

type MyDUnion = { type: "anon"; name: string } | { type: "google"; idToken: string };
type SpclCase = MyDUnion & {type: "anon"}

通过这样做,您可以获得有关这两种情况何时重叠的信息。现在,您可以直接为name键编制索引。

type Name = SpclCase['name']

如果你想让它更坚固,我们可以确保我们使用的窄型(在这种情况下{type: 'anon'})满足所需的形状。这里有一个小的即兴解决方案。

type DiscriminatorObj = { type: 'anon' }
type RequiredCase = DiscriminatorObj extends Pick<MyDUnion, "type"> ? DiscriminatorObj: never;
type SpclCase = (RequestBody & RequiredCase);
type Name = SpclCase['name']

我知道它的边缘有点粗糙,但你总是可以把它提取到泛型函数中,并随心所欲地使用它们。我只是向你展示了基本的逻辑。您可以使用它来使其更美观。

最新更新