Skip to content

Commit

Permalink
feat(command): add improved support for generic types part III (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed Mar 7, 2021
1 parent 3ecd38e commit 406afc2
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 29 deletions.
36 changes: 18 additions & 18 deletions command/command.ts
Expand Up @@ -80,22 +80,19 @@ interface IDefaultOption<

type ITypeMap = Map<string, IType>;
type OneOf<T, V> = T extends void ? V : T;
type Merge<T, V> = T extends void ? V : (V extends void ? T : T & V);

export class Command<
// deno-lint-ignore no-explicit-any
CO extends Record<string, any> | void = any,
// deno-lint-ignore no-explicit-any
CA extends Array<unknown> = CO extends number ? any : [],
// deno-lint-ignore no-explicit-any
CG extends Record<string, any> | void = CO extends number ? // deno-lint-ignore no-explicit-any
Record<string, any>
: void,
CG extends Record<string, any> | void = CO extends number ? any : void,
// deno-lint-ignore no-explicit-any
PG extends Record<string, any> | void = CO extends number ? // deno-lint-ignore no-explicit-any
Record<string, any>
: void,
PG extends Record<string, any> | void = CO extends number ? any : void,
// deno-lint-ignore no-explicit-any
P extends Command | undefined = CO extends void ? undefined : any,
P extends Command | undefined = CO extends number ? any : undefined,
> {
private types: ITypeMap = new Map<string, IType>([
["string", { name: "string", handler: new StringType() }],
Expand Down Expand Up @@ -243,20 +240,23 @@ export class Command<
* @param override Override existing child command.
*/
public command<
C extends Command<
C extends (CO extends number ? Command : Command<
// deno-lint-ignore no-explicit-any
Record<string, any> | void,
Array<unknown>,
// deno-lint-ignore no-explicit-any
Record<string, any> | void,
PG & CG | void | undefined,
Merge<PG, CG> | void | undefined,
OneOf<P, this> | undefined
>,
>),
>(
name: string,
cmd: C,
override?: boolean,
): C;
// deno-lint-ignore no-explicit-any
): C extends Command<infer O, infer A, infer G, any, any>
? Command<O, A, G, Merge<PG, CG>, OneOf<P, this>>
: never;
/**
* Add new sub-command.
* @param name Command definition. E.g: `my-command <input-file:string> <output-file:string>`
Expand All @@ -276,7 +276,7 @@ export class Command<
A,
// deno-lint-ignore no-explicit-any
CO extends number ? any : void,
PG & CG,
Merge<PG, CG>,
OneOf<P, this>
>;
/**
Expand Down Expand Up @@ -655,9 +655,9 @@ export class Command<
flags: string,
desc: string,
opts?:
| Omit<ICommandOption<Partial<CO>, CA, CG & G, PG, P>, "global">
| Omit<ICommandOption<Partial<CO>, CA, Merge<CG, G>, PG, P>, "global">
| IFlagValueHandler,
): Command<CO, CA, CG & G, PG, P> {
): Command<CO, CA, Merge<CG, G>, PG, P> {
if (typeof opts === "function") {
return this.option(flags, desc, { value: opts, global: true });
}
Expand All @@ -674,16 +674,16 @@ export class Command<
flags: string,
desc: string,
opts:
| ICommandOption<Partial<CO>, CA, CG & G, PG, P> & { global: true }
| ICommandOption<Partial<CO>, CA, Merge<CG, G>, PG, P> & { global: true }
| IFlagValueHandler,
): Command<CO, CA, CG & G, PG, P>;
): Command<CO, CA, Merge<CG, G>, PG, P>;
public option<O extends Record<string, unknown> | void = CO>(
flags: string,
desc: string,
opts?:
| ICommandOption<CO & O, CA, CG, PG, P>
| ICommandOption<Merge<CO, O>, CA, CG, PG, P>
| IFlagValueHandler,
): Command<CO & O, CA, CG, PG, P>;
): Command<Merge<CO, O>, CA, CG, PG, P>;
public option(
flags: string,
desc: string,
Expand Down
149 changes: 143 additions & 6 deletions command/test/command/generic_types_test.ts
Expand Up @@ -40,7 +40,7 @@ test({
.action((_options) => {
// callback fn should be valid also if args is not defined as second parameter.
})
// @ts-expect-error not allowed to add Command<any> to typed commands, must be casted to Command<void> or or a typed command.
// // @ts-expect-error not allowed to add Command<any> to typed commands, must be casted to Command<void> or or a typed command.
.command("bar", new Command())
.command("baz", new Command() as Command<void>)
.action((_options) => {
Expand Down Expand Up @@ -177,11 +177,7 @@ test({
test({
name: "command - generic types - splitted command with generics",
fn() {
const foo = new Command<
void,
[],
{ debug?: boolean; logLevel?: boolean }
>()
const foo = new Command<void, [], { debug?: boolean; logLevel?: boolean }>()
.globalOption<{ fooGlobal?: boolean }>("--foo-global", "")
.option<{ foo?: boolean }>("--foo", "")
.action((options) => {
Expand Down Expand Up @@ -293,6 +289,78 @@ test({
},
});

test({
name: "command - generic types - child command with parent option 1",
async fn() {
const fooCommand = new Command<void, [], void, { main?: boolean }>();

await new Command<void>()
.globalOption<{ main?: boolean }>("--main", "")
.command("foo", fooCommand)
.parse(Deno.args);
},
});

test({
name: "command - generic types - child command with parent option 2",
async fn() {
type GlobalOptions = { main?: boolean };

const foo = new Command<void>()
.option<{ foo?: boolean }>("-f, --foo", "");

const cmd = new Command<void>()
.globalOption<GlobalOptions>("--main", "")
.action((options) => {
/** valid */
options.main;
/** invalid */
// @ts-expect-error option foo not exists
options.foo;
})
.command("1", new Command<void, [], void, GlobalOptions>())
.action((options) => {
/** valid */
options.main;
/** invalid */
// @ts-expect-error option foo not exists
options.foo;
})
.command("2", new Command<void>())
.action((options) => {
/** valid */
options.main;
/** invalid */
// @ts-expect-error option foo not exists
options.foo;
})
.command("3", new Command<void, [], void, GlobalOptions>())
.action((options) => {
/** valid */
options.main;
/** invalid */
// @ts-expect-error option foo not exists
options.foo;
})
.command("4", foo)
.action((options) => {
/** valid */
options.main;
options.foo;
/** invalid */
// @ts-expect-error option foo not exists
options.bar;
});

cmd.command("5", new Command<void, [], void, { main?: boolean }>());

// @ts-expect-error unknown global option main2
cmd.command("6", new Command<void, [], void, { main2?: boolean }>());

await cmd.parse(Deno.args);
},
});

test({
name:
"command - generic types - child command with invalid parent option type",
Expand Down Expand Up @@ -381,3 +449,72 @@ test({
.parse(Deno.args);
},
});

test({
name: "command - generic types - constructor types",
fn() {
type Arguments = [input: string, output?: string, level?: number];
interface Options {
name: string;
age: number;
email?: string;
}
interface GlobalOptions {
debug?: boolean;
debugLevel: "debug" | "info" | "warn" | "error";
}

new Command<
Options,
Arguments,
GlobalOptions
>()
.arguments("<input:string> [output:string] [level:number]")
.globalOption("-d, --debug", "description ...")
.globalOption("-l, --debug-level <string>", "description ...", {
default: "warn",
})
.option("-n, --name <name:string>", "description ...", { required: true })
.option("-a, --age <age:number>", "description ...", { required: true })
.option("-e, --email <email:string>", "description ...")
.action((options, input, output?, level?) => {
/** valid options */
options.name && options.age && options.email;
options.debug && options.debugLevel;
if (level) {
isNaN(level);
}
/** invalid options */
// @ts-expect-error option foo does not exist.
options.foo;
// @ts-expect-error argument of type string is not assignable to parameter of type number
isNaN(input);
// @ts-expect-error argument of type string | undefined is not assignable to parameter of type number
isNaN(output);
// @ts-expect-error argument of type number | undefined is not assignable to parameter of type number
isNaN(level);
});
},
});

test({
name: "command - generic types - extended command",
async fn() {
class Foo extends Command {}

await new Command()
.command("foo", new Foo())
.parse(Deno.args);
},
});

// test({
// name: "command - generic types - extended command 2",
// async fn() {
// class Foo extends Command {}

// await new Command<void>()
// .command("foo", new Foo())
// .parse(Deno.args);
// },
// });
7 changes: 2 additions & 5 deletions command/types.ts
Expand Up @@ -11,10 +11,7 @@ import type { Command } from "./command.ts";

export type { IDefaultValue, IFlagValueHandler, ITypeHandler, ITypeInfo };

// type Merge<T, V> = T extends void ? V : (V extends void ? T : T & V);
// type OneOf<T, V> = T extends void ? V : T;
// type MergeOptions<PG, G, O> = Merge<PG, Merge<G, O>>;
// type NonVoidable<T> = T extends null | undefined | void ? Record<string, any> : T;
type Merge<T, V> = T extends void ? V : (V extends void ? T : T & V);

/* COMMAND TYPES */

Expand Down Expand Up @@ -46,7 +43,7 @@ export type IAction<
P extends Command | undefined = any,
> = (
this: Command<O, A, G, PG, P>,
options: PG & G & O,
options: Merge<Merge<PG, G>, O>,
...args: A
) => void | Promise<void>;

Expand Down

0 comments on commit 406afc2

Please sign in to comment.