如何在 TypeScript 中为交集类型分配值

huangapple go评论63阅读模式
英文:

How to assign value to a intersection type in typescript

问题

我正在尝试使用TypeScript编写一个VSCode扩展,我有一个ChildProcess并且想要终止/杀死它。

export declare function terminate(process: ChildProcess & {
    pid: number;
}, cwd?: string): boolean;
...
let vlsProcess: ChildProcess

然后我尝试调用

terminate(vlsProcess);

但是我遇到了这个错误:

类型"ChildProcess"的参数不能赋值给类型"ChildProcess & { pid: number; }"的参数。
类型"ChildProcess"不能赋值给类型"{ pid: number; }"。
在类型"ChildProcess"中,属性"pid"是可选的,但在类型"{ pid: number; }"中是必需的。

函数terminate要求一个"交叉类型",如下所示:

ChildProcess & {
    pid: number;
}

但我目前只有一个ChildProcess,如何将ChildProcess转换为ChildProcess & { pid: number; }

我检查了ChildProcess,它有如下定义:

readonly pid?: number | undefined;

所以在我看来,它似乎可以转换为ChildProcess & { pid: number; },但是TypeScript编译器说:

属性"pid"在类型"ChildProcess"中是可选的,但在类型"{ pid: number; }"中是必需的。

我该如何进行这种转换?

英文:

I'm trying to write a vscode extension in typescript, I have a ChildProcess and I want to terminate/kill it.

export declare function terminate(process: ChildProcess & {
    pid: number;
}, cwd?: string): boolean;
...
let vlsProcess: ChildProcess

and then I tried to call

terminate(vlsProcess);

but I have this error:
>Argument of type 'ChildProcess' is not assignable to parameter of type 'ChildProcess & { pid: number; }'.
Type 'ChildProcess' is not assignable to type '{ pid: number; }'.
Property 'pid' is optional in type 'ChildProcess' but required in type '{ pid: number; }'

the function terminate is expecting an "intersection type" of

ChildProcess & {
    pid: number;
}

but I currently only have a ChildProcess, how can I convert a ChildProcess into ChildProcess & { pid: number;}?

I checked the ChildProcess, it has

readonly pid?: number | undefined;

so to me, it seems to be able to convert to a ChildProcess & { pid: number;} but the typescript compiler says:
>Property 'pid' is optional in type 'ChildProcess' but required in type '{ pid: number; }'

How can I do this convertion?

答案1

得分: 1

"terminate(vlsProcess)" 编译无错误的方法是说服编译器,"vlsProcess" 是类型为 "ChildProcess & { pid: number; }" 的,这意味着它必须是一个 "ChildProcess",其中 "pid" 属性已知存在且为 "number" 类型。但是,"vlsProcess" 被声明为一个 "ChildProcess",其 "pid" 属性是可选的。因此,您需要对 "pid" 属性采取一些操作。

一种方法是在调用 "terminate(vlsProcess)" 之前编写一个检查,以确保 "typeof vlsProcess.pid === 'number'",希望这样的检查可以缩小 "vlsProcess" 的表现类型。不幸的是,这并不起作用:

if (typeof vlsProcess.pid === 'number') {
  terminate(vlsProcess); // 仍然报错 😢
}

这实际上是 TypeScript 的一个缺失功能,如 microsoft/TypeScript#42384 中所描述的。虽然检查 "typeof vlsProcess.pid === 'number'" 可以将 "vlsProcess.pid" 的表现类型从 "number | undefined" 缩小为 "number",但它对 "vlsProcess" 本身的表现类型没有影响。通常情况下,让某个子属性的检查(例如 "a.b.c.d.e.f")对所有父对象产生影响将会很昂贵,因为编译器需要花费时间合成所有相关的类型,而其中大多数对于大多数调用来说都是完全无用的。

在没有更好的解决方法之前,我们可以通过实现自定义类型守卫函数来模拟这种缩小行为。就像这样:

function hasDefinedProp<T extends object, K extends PropertyKey>(
  obj: T, k: K
): obj is T & { [P in K]: {} | null } {
  return (typeof (obj as any)[k] !== 'undefined');
}

如果 "hasDefinedProp(obj, k)" 返回 "true",那么 "obj" 将从其原始类型缩小为一个子类型,该子类型已知在 "k" 键上有一个定义的属性。这被写成 "T" 和 "{ [P in K]: {} | null }" 的交叉类型,后者等同于使用 "Record<K, {} | null>" 的类型,使用 Record 实用工具类型。类型 "{ [P in K]: {} | null }" 已知在类型 "K" 的每个键上都有一个类型为 "{} | null" 的属性,而 "{} | null" 本质上允许除 "undefined" 之外的所有值。与类型交叉后,将从其领域中消除 "undefined",在 TypeScript 4.8 中引入

请注意,实现中使用了 类型断言 "obj as any",以允许我们在不报错的情况下索引 "obj[key]"。

好的,现在让我们试一下:

if (hasDefinedProp(vlsProcess, 'pid')) {
  vlsProcess // ChildProcess & { pid: {} | null }
  terminate(vlsProcess); // 可行
}

这样可以正常工作。TypeScript 看到缩小的类型 "ChildProcess & { pid: {} | null }" 可以赋值给 "ChildProcess & { pid: number }",因为前者的 "pid" 属性是 "(number | undefined) & ({} | null)",这等于 "number"。

如果因某种原因 "hasDefinedProp()" 返回 "false",则不应调用 "terminate(vlsProcess)",因为 "vlsProcess" 没有 "pid"。在这种情况下,您应该根据您的用例采取适当的操作。在上述示例中,它只是跳过了 "terminate()" 调用,但您可能希望抛出异常或采取其他操作。

[代码演示链接](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAE2AYwDYEMrDgMwK4B2yMAlhAXDMFALYkHpUAUYUEywAzpwFxwDCACxKoEABTYducAGRwA3gFgAUHDhgSCPgTw0ARtQDcKgL4AaOMgDuCAPx9OMKPQDmASj56IEVMHQFjZRV6KigcdA4BYVEJdi5OBRU1LHQEclQAT3VNezgdfWo4AB84QiQcemAEQLUIGEFqABVhAhdeOEdnVsCTFRUkNExsX3gAN1ROWKl2oRFxSXi+5XwiUnI4QXROABFgCoIq2LAAHka4UCoCBASIPQArFBgLAGlzkEvruCPqGAzn4AyAD4mEk4Lc7nxGhYANZ8Z4qDxg+5wEgJM5yeRwADaYhRFGeAF0+PITMU8nhUKg4KSlKo4FgYHgoBQmL8wMAIDg4ExwXAtnyCBk3FjoQS4ABCAC8krgACIyntKghZW4eksSFzWRl2Zy4ONJgtuAA6DQIODSmWy-IGKAqxJ00J0BjMfVTeKquAAek9HXQNGw1DYUDggF4NwBEe6Z1ZrNjtFQd5hAwExXYbOBZZaaVW57WoU3FpN6onM3dIMdktApSSUdJS4IZeg7qE7GMBkxMS5wPYWIND0BlTEA

英文:

The way for terminate(vlsProcess) to compile without error is to convince the compiler that vlsProcess is of type ChildProcess &amp; { pid: number; }, meaning it has to be a ChildProcess where the pid property is known to exist and be of type number. But vlsProcess is declared a ChildProcess whose pid property is optional. So you need to do something with that pid property.

One approach would be to write a check that typeof vlsProcess.pid === &quot;number&quot; before you call terminate(vlsProcess), in the hopes that such a check would narrow the apparent type of vlsProcess. Unfortunately that doesn't work:

if (typeof vlsProcess.pid === &quot;number&quot;) {
  terminate(vlsProcess); // same error &#128546;
}

This is essentially a missing feature of TypeScript, as described in microsoft/TypeScript#42384 While the check typeof vlsProcess.pid === &quot;number&quot; can narrow the apparent type of vlsProcess.pid from number | undefined to number, it has no effect on the apparent type of vlsProcess itself. In general it would be too expensive to have a check of some subproperty like a.b.c.d.e.f have effects on all the parent objects, since the compiler would need to spend time synthesizing all the relevant types, most of which would be completely useless for most calls.


Until and unless something better happens there, we can luckily emulate this sort of narrowing by implementing a custom type guard function. Like this:

function hasDefinedProp&lt;T extends object, K extends PropertyKey&gt;(
  obj: T, k: K
): obj is T &amp; { [P in K]: {} | null } {
  return (typeof (obj as any)[k] !== &quot;undefined&quot;);
}

If hasDefinedProp(obj, k) returns true, then obj will be narrowed from its original type to a subtype which is known to have a defined property at the k key. This is written as an intersection of T and { [P in K]: {} | null }, the latter being equivalent to Record&lt;K, {} | null&gt; using the Record utility type. The type { [P in K]: {} | null } is known to have a property of type {} | null at every key of type K, and {} | null essentially allows every value except for undefined. Intersecting a type with {} | null will serve to eliminate undefined from its domain, as introduced in TypeScript 4.8.

Note that the implementation uses the type assertion obj as any to allow us to index into obj[key] without complaint.


Okay, now let's try it:

if (hasDefinedProp(vlsProcess, &quot;pid&quot;)) {
  vlsProcess // ChildProcess &amp; { pid: {} | null }
  terminate(vlsProcess); // okay
}

That works. TypeScript sees the narrowed type ChildProcess &amp; { pid: {} | null } to be assignable to ChildProcess &amp; { pid: number }, because the pid property of the former is (number | undefined) &amp; ({} | null) which is number.

And if, for some reason, hasDefinedProp() returns false, then you don't want to call terminate(vlsProcess) because vlsProcess has no pid. What you should do in such a situation depends on your use case. In the above it just skips the terminate() call, but you might want to throw an exception or something.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月19日 09:01:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76503090.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定