我有一个有区别的联合
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
这是无效的,因为这样我将不得不导出MyName
和MyDUnion
类型以在其他地方使用。
有什么想法吗?
为了过滤对象的联合,通常需要使用 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 access
和keyof
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;
} // ✅ */
陷阱
不区分
undefined
和optional
属性。虽然可以做到,但它在待办事项上。直接提取文本。
别这样:
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']
我知道它的边缘有点粗糙,但你总是可以把它提取到泛型函数中,并随心所欲地使用它们。我只是向你展示了基本的逻辑。您可以使用它来使其更美观。