Takeshi Tokugawa YD
Takeshi Tokugawa YD

Reputation: 923

Access to type declarations from dependencies in TypeScript

Here is the types for arg, Node.js console commands parser:

declare const flagSymbol: unique symbol;

declare function arg<T extends arg.Spec>(spec: T, options?: arg.Options): arg.Result<T>;

declare namespace arg {
    export function flag<T>(fn: T): T & { [flagSymbol]: true };

    export const COUNT: Handler<number> & { [flagSymbol]: true };

    export type Handler <T = any> = (value: string, name: string, previousValue?: T) => T;

    export interface Spec {
        [key: string]: string | Handler | [Handler];
    }

    export type Result<T extends Spec> = { _: string[] } & {
        [K in keyof T]?: T[K] extends Handler
            ? ReturnType<T[K]>
            : T[K] extends [Handler]
            ? Array<ReturnType<T[K][0]>>
            : never
    };

    export interface Options {
        argv?: string[];
        permissive?: boolean;
        stopAtPositional?: boolean;
    }
}

export = arg;

I need to annotate parsing results:

import parseConsoleArgument from 'arg';

export function cli(rawConsoleCommandData: Array<string>): void {
  const consoleCommandArguments: /* ??? */ = parseConsoleArgument(
      {}, { argv: rawConsoleCommandData.slice(2)}
  );
}

Logically correct type is arg.Result. However, I can not access to it. Below code

import parseConsoleArgument from 'arg';

export function cli(rawConsoleCommandData: Array<string>): void {
  const consoleCommandArguments: arg.Result = parseConsoleArgument(
      {}, { argv: rawConsoleCommandData.slice(2)}
  );
}

gives TS2503: Cannot find namespace "arg".

Upvotes: 1

Views: 156

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249546

Not sure why you want to annotate the variable, its type will be inferred based on the result of parseConsoleArgument. So it will already be of the appropriate type. If your team requires annotations on all variables you should challenge that IMO, there are lots of types that are very difficult to annotate without duplicating a lot of code. This is one of those cases, lets see why.

Accessing the Result is not an issue, you just don't access it through the arg namespace, you need to access it through the module alias you used:

const consoleCommandArguments: parseConsoleArgument.Result<{}> = parseConsoleArgument(
    {}, { argv: rawConsoleCommandData.slice(2)}
);

This works, but you see you have a type parameter for parseConsoleArgument.Result that I used {} for. This might not seem like an issue until you start adding arguments to be parsed:

export function cli(rawConsoleCommandData: Array<string>): void {
  const consoleCommandArguments: parseConsoleArgument.Result<{}> = parseConsoleArgument(
      { p: Number, p2: Boolean }, { argv: rawConsoleCommandData.slice(2)}
  );
  consoleCommandArguments.p // err
}

If we remove the explicit annotation we don't get an error, and p is a number as one might expect. This happens because parseConsoleArgument is a generic function that extracts type information from it's first parameter and uses conditional types to create a new type that has { p: number, p2: Boolean }

If we want to type this manually we would need to write:

export function cli(rawConsoleCommandData: Array<string>): void {
  const consoleCommandArguments : parseConsoleArgument.Result<{
    p: typeof Number,
    p2: typeof Boolean
  }> = parseConsoleArgument(
      { p: Number, p2: Boolean }, { argv: rawConsoleCommandData.slice(2)}
  );
  consoleCommandArguments.p // ok
}

Which works but is ugly and much longer than the original (needlessly so) and requires us to change two places to add a new parameter. I would let the compiler infer the type, it is pretty good at it. Anyone wanting to see the actual type can just hover over the variable and they will see the type in a tooltip

Upvotes: 1

Related Questions