Skip to content

Commit

Permalink
feat(command): add improved support for generics types part II (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed Mar 6, 2021
1 parent bc93ae5 commit e143799
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 66 deletions.
46 changes: 31 additions & 15 deletions command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,21 @@ interface IDefaultOption<
}

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

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 void ? [] : any,
CA extends Array<unknown> = CO extends number ? any : [],
// deno-lint-ignore no-explicit-any
CG extends Record<string, any> | void = Record<string, any> | void,
CG extends Record<string, any> | void = CO extends number ? // deno-lint-ignore no-explicit-any
Record<string, any>
: void,
// deno-lint-ignore no-explicit-any
PG extends Record<string, any> | void = Record<string, any> | void,
PG extends Record<string, any> | void = CO extends number ? // deno-lint-ignore no-explicit-any
Record<string, any>
: void,
// deno-lint-ignore no-explicit-any
P extends Command | undefined = CO extends void ? undefined : any,
> {
Expand Down Expand Up @@ -241,10 +244,12 @@ export class Command<
*/
public command<
C extends Command<
Record<string, unknown> | void,
// deno-lint-ignore no-explicit-any
Record<string, any> | void,
Array<unknown>,
Record<string, unknown> | void,
Merge<PG, CG> | void,
// deno-lint-ignore no-explicit-any
Record<string, any> | void,
PG & CG | void | undefined,
OneOf<P, this> | undefined
>,
>(
Expand All @@ -258,11 +263,22 @@ export class Command<
* @param desc The description of the new child command.
* @param override Override existing child command.
*/
public command<A extends Array<unknown> = []>(
public command<
// deno-lint-ignore no-explicit-any
A extends Array<unknown> = Array<any>,
>(
name: string,
desc?: string,
override?: boolean,
): Command<void, A, void, Merge<PG, CG>, OneOf<P, this>>;
): Command<
// deno-lint-ignore no-explicit-any
CO extends number ? any : void,
A,
// deno-lint-ignore no-explicit-any
CO extends number ? any : void,
PG & CG,
OneOf<P, this>
>;
/**
* Add new sub-command.
* @param nameAndArguments Command definition. E.g: `my-command <input-file:string> <output-file:string>`
Expand Down Expand Up @@ -639,9 +655,9 @@ export class Command<
flags: string,
desc: string,
opts?:
| Omit<ICommandOption<Partial<CO>, CA, Merge<CG, G>, PG, P>, "global">
| Omit<ICommandOption<Partial<CO>, CA, CG & G, PG, P>, "global">
| IFlagValueHandler,
): Command<CO, CA, Merge<CG, G>, PG, P> {
): Command<CO, CA, CG & G, PG, P> {
if (typeof opts === "function") {
return this.option(flags, desc, { value: opts, global: true });
}
Expand All @@ -658,16 +674,16 @@ export class Command<
flags: string,
desc: string,
opts:
| ICommandOption<Partial<CO>, CA, Merge<CG, G>, PG, P> & { global: true }
| ICommandOption<Partial<CO>, CA, CG & G, PG, P> & { global: true }
| IFlagValueHandler,
): Command<CO, CA, Merge<CG, G>, PG, P>;
): Command<CO, CA, CG & G, PG, P>;
public option<O extends Record<string, unknown> | void = CO>(
flags: string,
desc: string,
opts?:
| ICommandOption<Merge<CO, O>, CA, CG, PG, P>
| ICommandOption<CO & O, CA, CG, PG, P>
| IFlagValueHandler,
): Command<Merge<CO, O>, CA, CG, PG, P>;
): Command<CO & O, CA, CG, PG, P>;
public option(
flags: string,
desc: string,
Expand Down
137 changes: 86 additions & 51 deletions command/test/command/generic_types_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ test({
args.foo;
// @ts-expect-error option foo does not exists
options.foo;
// @ts-expect-error expression of type void cannot be tested for truthiness
if (options) {
// @ts-expect-error option foo does not exists
options.foo;
}
})
.command("foo", new Command<void>())
.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.
.command("bar", new Command())
.command("baz", new Command() as Command<void>)
.action((_options) => {
// callback fn should be valid also if args is not defined as second parameter.
});
Expand Down Expand Up @@ -206,7 +216,7 @@ test({
.select("bar-bar")
.reset();

const bar = new Command<void, [], { debug: boolean; logLevel: boolean }>()
const bar = new Command<void, [], { debug?: boolean; logLevel?: boolean }>()
.globalOption<{ barGlobal?: boolean }>("--bar-global", "")
.option<{ bar: boolean }>("--bar", "")
.versionOption("--versionx", "", {
Expand Down Expand Up @@ -237,7 +247,7 @@ test({
options.fooFoo && options.fooFooGlobal;
});

const _main: Command = new Command<void, []>()
const _main: Command = new Command<void>()
.arguments<[input: string, output: string]>("<input> [output]")
.globalOption<{ debug?: boolean }>("--debug", "")
.globalOption<{ logLevel?: boolean }>("--log-level", "")
Expand Down Expand Up @@ -297,52 +307,77 @@ test({
},
});

// @TODO: unknown global options doesn't emit an type checking error.
// test({
// name: "command - generic types - child command with invalid parent option",
// async fn() {
// const fooCommand = new Command<void, [], void, { main2?: boolean }>();
//
// await new Command<void>()
// .option<{ main?: boolean }>("-d, --debug", "...", { global: true })
// // @ts-expect-error unknown global option main2
// .command("foo", fooCommand)
// .parse(Deno.args);
// },
// });
//
// test({
// name: "command - generic types - child command with invalid parent option 2",
// async fn() {
// const fooCommand = new Command<void, [], void, { main2?: boolean }>();
//
// await new Command<void>()
// .option<{ main?: boolean }>("--main", "")
// // @ts-expect-error unknown global option main2
// .command("foo", fooCommand)
// .parse(Deno.args);
// },
// });
//
// test({
// name: "command - generic types - child command with invalid parent option 3",
// async fn() {
// const fooCommand = new Command<void, [], void, { main2?: boolean }>();
//
// await new Command<void>()
// .option<{ main?: boolean }>("--main", "")
// // @ts-expect-error unknown global option main2
// .command("foo", fooCommand)
// .parse(Deno.args);
// },
// });
//
// test({
// name: "command - generic types - child command with invalid parent option",
// async fn() {
// await new Command<void>()
// // @ts-expect-error unknown global option main
// .command("foo", new Command<void, [], void, { main?: boolean }>())
// .parse(Deno.args);
// },
// });
test({
name: "command - generic types - child command with invalid parent option 1",
async fn() {
const fooCommand = new Command<void, [], void, { main2?: boolean }>();

await new Command<void>()
.option<{ main?: boolean }>("-d, --debug", "...", { global: true })
// @ts-expect-error unknown global option main2
.command("foo", fooCommand)
.parse(Deno.args);
},
});

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

await new Command<void>()
.globalOption<{ main?: boolean }>("-d, --debug", "...")
// @ts-expect-error unknown global option main2
.command("foo", fooCommand)
.parse(Deno.args);
},
});

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

await new Command<void>()
.option<{ main?: boolean }>("--main", "")
// @ts-expect-error unknown global option main2
.command("foo", fooCommand)
.parse(Deno.args);
},
});

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

await new Command<void>()
.option<{ main?: boolean }>("--main", "")
// @ts-expect-error unknown global option main2
.command("foo", fooCommand)
.parse(Deno.args);
},
});

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

await new Command<void>()
.option<{ main?: boolean }>("--main", "")
// @ts-expect-error unknown global option main
.command("foo", fooCommand)
.parse(Deno.args);
},
});

test({
name: "command - generic types - child command with invalid parent option 6",
async fn() {
await new Command<void>()
// @ts-expect-error unknown global option main
.command("foo", new Command<void, [], void, { main?: boolean }>())
.parse(Deno.args);
},
});

0 comments on commit e143799

Please sign in to comment.