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?
The way for terminate(vlsProcess)
to compile without error is to convince the compiler that vlsProcess
is of type ChildProcess & { 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 === "number"
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 === "number") {
terminate(vlsProcess); // same error 😢
}
This is essentially a missing feature of TypeScript, as described in microsoft/TypeScript#42384 While the check typeof vlsProcess.pid === "number"
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<T extends object, K extends PropertyKey>(
obj: T, k: K
): obj is T & { [P in K]: {} | null } {
return (typeof (obj as any)[k] !== "undefined");
}
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<K, {} | null>
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, "pid")) {
vlsProcess // ChildProcess & { pid: {} | null }
terminate(vlsProcess); // okay
}
That works. TypeScript sees the narrowed type ChildProcess & { pid: {} | null }
to be assignable to ChildProcess & { pid: number }
, because the pid
property of the former is (number | undefined) & ({} | 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.