typescriptvscode-extensionsintersection-types

How to assign value to a intersection type in typescript


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?


Solution

  • 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.

    Playground link to code