仅通过属性上的类型保护来细化父对象

2023-12-06

我有一个类型Box<T> where T将永远是延伸的东西State这是一个受歧视的工会。

type StateA = { type: "A"; a: number };
type StateB = { type: "B"; b: string };
type State = StateA | StateB;

interface Box<T extends State = State> {
  state: T;
}

我现在想检查一下什么样的Box变量是并将类型参数缩小到更具体的类型。检查受歧视工会类型作品类型的幼稚方法。通常的类型推断使用if or switch当立即访问装箱状态的属性时,语句可以正常工作。但该类型的Box不缩小,只缩小类型Box.state.

const b: Box = { state: { type: "B", b: "str" } };
if (b.state.type === "B") {
  console.log(b.state.b); // Inferred correctly
  const bRefined: Box<StateB> = b; // Assignment not possible
}

不过,这可以通过用户定义的类型保护来解决:

function isBoxB(b: Box): b is Box<StateB> {
  return b.state.type === "B";
}

if (isBoxB(b)) {
  console.log(b.state.b); // Inferred correctly
  const bRefined: Box<StateB> = b; // Assignment possible
}

我可以接受这种解决方法,但我对此并不满意。有没有更好的办法自动缩小周围的类型Box无需编写自定义类型保护?

整个代码可以在 Typescript Playground 上找到。


在打字稿中,缩小一般来说,对象属性(或子属性、子子属性等)的表观类型不会缩小对象本身的表观类型。唯一发生这种情况的情况是该对象属于受歧视联盟类型和您正在检查的属性是它的判别式.

就你而言,虽然state的财产Box属于受歧视联盟类型State, Box本身并不是一个受歧视的工会。这根本就不是一个工会。所以如果你有一个价值b类型的Box,那么即使检查b.state.type会缩小b.state,它不会缩小b itself.

这是 TypeScript 的一个已知限制,并且已被多次报告。目前需要改进的开放问题是微软/TypeScript#42384。在过去,这些只是因为修复成本太高而被关闭(为了检查a.b.c.d.e缩小范围不仅仅是a.b.c.d.e,编译器可能需要合成新类型来表示对a.b.c.d, a.b.c, a.b, and a). A comment表明情况可能已经发生变化并且is可以实施。但目前还没有实施。

除非这种情况发生变化,否则我们就必须解决它。


有时对人们有用的一种解决方法是将现有对象复制到新对象中,其中选中的属性被挑出以进行显式复制。这将引导编译器完成合成缩小类型的逻辑。在你的情况下,它看起来像这样:

if (b.state.type === "B") {
  const bRefined: Box<StateB> = { ...b, state: b.state } // okay
}

在这里我们有spread现有的b对象转换为新的对象字面量,然后显式复制state财产结束。现在编译器看到{...b, state: b.state}属于类型Box<StateB>.


否则,一般的解决方法是构建一个自定义类型保护函数它封装了“检查属性应该缩小父级”的概念。它可能看起来像这样:

function hasPropType<T extends object, K extends keyof T, V extends T[K]>(
  obj: T, key: K, valGuard: (x: T[K]) => x is V): obj is T & Record<K, V> {
  return valGuard(obj[key]);
}

这告诉编译器如果valGard(obj[key]) is true, then hasPropType(obj, key, valGuard)可以缩小类型obj。在您的示例中,它可能如下所示:

if (hasPropType(b, "state", (x): x is StateB => x.type === "B")) {
  const bRefined: Box<StateB> = b; // okay
}

当然,对于单次使用来说,这比仅仅使用您的isBoxB()方法。但是,如果您发现自己经常进行此类检查,则可能不需要为每个检查构建单独的类型保护函数。


这里的一个折衷方案可能是编写一个比isBoxB但不那么一般hasPropType:

function isBox<K extends State['type']>(type: K, b: Box
): b is Box<Extract<State, { type: K }>> {
  return b.state.type === type;
}

if (isBox("B", b)) {
  const bRefined: Box<StateB> = b; // okay
}

Playground 代码链接

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

仅通过属性上的类型保护来细化父对象 的相关文章