Typescript中部分类型的并集不能缩小类型

时间:2019-09-04 00:31:58

标签: typescript union-types

我有一个联合类型,表示用户可以使用表单字段构造的一段数据。基本流程是用户选择他们想做的事情,然后提供正确的UI,并在他们编辑表单字段时更新存储的对象。进行中的对象表示为联合类型的部分化版本,具体取决于用户选择制作的事物的类型。我想用两种方式来引用Partialized类型的类型,但是都有问题。

代码段可以解释更多,但是基本上,回调的签名和类型防护是我想引用进行中值的两种方法。在我提出的两个方案中,都定义了Partialized联合类型,这两个用例之一无法编译。

似乎变体一更正确,更精确,所以令我惊讶的是它未能正确编译。我想尽可能避免转换,以使此代码尽可能地健壮,以便向联合类型添加更多成员。

export interface Key {
  type: "key";
  key: string;
}

export interface KeyValue {
  type: "key-value";
  key: string;
  value: string;
}

export type Either = Key | KeyValue;

export type Common = Pick<Either, "type" | "key">;
export const Common = {
  toString: ({ type, key }: Common): string => null as any,
  fromString: (s: string): Common => null as any,
};

// USE CASE 1: This does not work when using variant one, below.

const callback: (v: PartialEither) => void = null as any;
callback(Common.fromString(""));

// USE CASE 2: This does not work when using variant two, below.
// This makes sense, since variant two's definition drops the relationship between `type` 
// and the corresponding object shape, so type narrowing can't work.

const either: PartialEither = null as any;
if (either.type === "key-value") {
  either.value;
}

// VARIANT ONE
// Comment this out and replace it with variant two to see the errors change, above.

// Using this intermediate type so I can still rely on 'type' as the discriminant property of
// the PartialEither type.
type _PartialEither<T extends Either> = Pick<T, "type" | "key"> & Partial<Omit<T, "type" | "key">>;

export type PartialKey = _PartialEither<Key>;
export type PartialKeyValue = _PartialEither<KeyValue>;

export type PartialEither = PartialKey | PartialKeyValue;

// VARIANT TWO
// Uncomment this out replace variant one with it to see the errors change, above.

// type PartialEither = Pick<Either, "type" | "key"> & Partial<Omit<Either, "type" | "key">>

(playground link)

1 个答案:

答案 0 :(得分:1)

对于编译器为什么不理解您的PartialEither类型,我没有很深入的了解;像这样的差距涉及交集和映射类型(如microsoft/TypeScript#18538)并不是前所未有的……这并不是问题所在。无论如何,我的直觉是,无论是什么问题,它更像是TypeScript中的错误/设计限制,而不太像是您的代码问题。不确定是否存在与此相关的问题,或者您是否想打开它。


不过,如果我想在这里继续,我会尝试将PartialEither转换为我能想到的最简单的类型……没有交集的对象类型的并集。一种实现方法是使用我称为Expand的实用程序类型:

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

这会将具体对象类型的交集转换为单个对象类型(因此{a: string} & {b: number}将变为{a: string; b: number}),并且也适用于联合。它不会递归到对象属性中。可以编写一个ExpandRecursive来做到这一点,但是我们不需要它。

然后我们可以这样做:

export type PartialEither = Expand<PartialKey | PartialKeyValue>;

看到现在在IntelliSense中观察到的PartialEither类型是:

type PartialEither = {
    type: "key";
    key: string;
} | {
    type: "key-value";
    key: string;
    value?: string | undefined;
}

当您这样做时,错误就会消失。那是我的建议。


回到“这似乎是TypeScript中的错误或设计限制”主题:

请注意,编译器确实认为PartialKey | PartialKeyValueExpand<PartialKey | PartialKeyValue>是可相互分配的类型,否则将出现错误:

type MutuallyAssignable<T extends U, U extends V, V = T> = true;
type Okay = MutuallyAssignable<PartialKey | PartialKeyValue, PartialEither>; // no error

但是在给它们分配类型Common的值时,编译器对其中一个感到满意,而对另一个感到不满意:

function hmm(common: Common) {
  let nope: PartialKey | PartialKeyValue = common; // error
  let yep: PartialEither = common; // okay
  yep = nope; // okay also ?
}

因此,这里的编译器类型分析中肯定有一些棘手的问题。如果我发现更多信息(例如与此相关的现有问题),我将进行更新;否则...祝你好运!

更新:此可能microsoft/TypeScript#19927有关,但不确定。我确实看到您的PartialEither里面有Pick<Key, never>,但是我不能确定是否是同一问题?

Link to code