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



我有一个类型Box<T>,其中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,并将类型参数缩小到更具体的类型。检查受歧视工会类型的幼稚方法有效。使用ifswitch语句的常规类型推断在立即访问装箱状态的属性时工作正常。但是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 上找到。

在 TypeScript 中,缩小对象属性(或子属性,或子子属性等)的表观类型通常不会缩小对象本身的表观类型。 唯一发生这种情况的情况是,如果对象是可区分的联合类型,并且您正在检查的属性是其判别式的。

在你的情况下,虽然Boxstate属性属于State的可区分联合类型,但Box本身不是歧视性联合。 这根本不是工会。因此,如果你有一个类型Box的值b,那么即使选中b.state.type会缩小b.state,它也不会缩小b本身。

这是 TypeScript 的一个已知限制,并且已被多次报告。 目前要求改进的问题是microsoft/TypeScript#42384。在过去,这些只是因为修复成本太高而被关闭(为了使a.b.c.d.e的检查范围缩小到不仅仅是a.b.c.d.e,编译器可能需要合成新的类型来表示对a.b.c.da.b.ca.ba的影响)。 评论表明,也许情况已经发展,可以实施。但目前尚未实施。

除非这种情况发生变化,否则我们只需要解决它。


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

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

在这里,我们将现有的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])true,那么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
}

操场链接到代码

最新更新