Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
eegli committed Mar 1, 2024
1 parent f80428e commit ff49af9
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-numbers-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@eegli/tinyparse": patch
---

Fix Handler utility types to preserve handler return type
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ jobs:
- name: Install dependencies
run: yarn install --immutable
- name: Run unit test
run: yarn test --coverage --verbose
run: yarn test:unit
- name: Run type test
run: yarn test:types
- name: Run integration tests
if: matrix.node == '20'
run: yarn test:integration
- name: Run type test
run: yarn test:types
- name: Run build
run: yarn build
- name: Upload coverage to Codecov
Expand Down
32 changes: 22 additions & 10 deletions docs/reference/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ Because you might want to do things _before_ invoking a handler, `.parse([...arg

## Declaring Handlers

A default handler can be declared either inline or externally, just like a subcommand handler. The difference between a subcommand and default handler is that the default handler has no constraint on the number of arguments it accepts. It simply receives all positional arguments. Other than that, it also has access to the globals, options and usage text. All handlers can be _asynchronous_.
A default handler can be declared either inline or externally, just like a subcommand handler. The difference between a subcommand and default handler is that the default handler has no constraint on the number of arguments it accepts. It simply receives all positional arguments. Other than that, it also has access to the globals, options and usage text.

- All handlers can be _asynchronous_ - they are always awaited
- Handlers can return _any value_ - it is ignored

```ts
import { Parser } from '@eegli/tinyparse';
Expand All @@ -49,7 +52,11 @@ Default handlers can be a good place to handle things that Tinyparse is not opin

## Modular Declaration

In the above example, we use the `CommandHandler` type to declare the default handler. This will annotate the function as taking an object literal with all parameters. Hence, if you want to invoke it somewhere else - like in a test - you will need to pass values for `args`, `globals`, `options`, and `usage`. This is a bit cumbersome. You can declare default and subcommand handlers more modularly by specifiying _exactly_ what they need using the `Handler*` utility types:
In the above example, we use the `CommandHandler` utility type to declare the default handler. This will annotate the handler as a function taking an object literal with all parameters and returning anything.

However, your handler might return something that you'd like to assert in a testing scenario, and it might not need all parameters. With the `CommandHandler` type, you would need to pass values for `args`, `globals`, `options`, and `usage`. This is a bit cumbersome.

To solve this, you can **declare default and subcommand handlers more modularly** by specifiying _exactly_ what parameters they need using the `Handler*` utility types. This will also preserve the return type:

```ts
import { Parser } from '@eegli/tinyparse';
Expand All @@ -59,16 +66,21 @@ import type {
HandlerOptions,
} from '@eegli/tinyparse';

const options = new Parser();
const options = new Parser().option('foo', {
longFlag: '--foo',
defaultValue: 'default',
});

type Globals = HandlerGlobals<typeof options>;
type Options = HandlerOptions<typeof options>;

// Handler that only needs options and globals
type DefaultHandler = HandlerParams<Options, never, Globals>;
type Params = HandlerParams<Options, never, Globals>;

const defaultHandler: DefaultHandler = ({ globals, options }) => {
const defaultHandler = ({ globals, options }: Params) => {
console.log({ globals, options });
// In a test, assert that true is returned
return true;
};

// Other possible options...
Expand All @@ -78,12 +90,12 @@ type NoParamsHandler = HandlerParams;
const noopHandler: NoParamsHandler = () => {};

// Positional arguments and options only
type HandlerWithArgs = HandlerParams<Options, [string]>;
const handlerWithArgs: HandlerWithArgs = ({ options, args }) => {};
type ParamsWithArgs = HandlerParams<Options, [string]>;
const handlerWithArgs = ({ options, args }: ParamsWithArgs) => {};

// Usage only
type HandlerWithUsage = HandlerParams<never, never, never, string>;
const handlerWithUsage: HandlerWithUsage = ({ usage }) => {};
type ParamsWithUsage = HandlerParams<never, never, never, string>;
const handlerWithUsage = ({ usage }: ParamsWithUsage) => {};
```

In the above example, we specify a handful of handlers, each of which only needs a subset of what Tinyparse feeds it. `HandlerParams` has the following signature:
Expand All @@ -97,4 +109,4 @@ type HandlerParams<
> = (...)
```
In summary, the `CommandHandler` can be a useful shortcut, but if you only need a subset of the parameters, it might make sense to declare the handler more modularly.
In summary, the `CommandHandler` can be a useful shortcut, but if you only need a subset of the parameters and/or would like to preserve the return type, it might make sense to declare the handler more modularly.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"release": "yarn build:release && changeset publish",
"test": "jest --verbose",
"test:coverage": "jest --coverage",
"test:integration": "yarn build && cd test/integration && npm install --omit=dev --ignore-scripts && tsc && npm test",
"test:types": "tstyche",
"test": "yarn test:unit && yarn test:types",
"test:unit": "jest --selectProjects unit",
"test:types": "tstyche",
"test:integration": "yarn build && cd test/integration && npm install --omit=dev --ignore-scripts && tsc && npm test",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
},
"lint-staged": {
Expand Down
19 changes: 10 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ export type CommandOptionsMap<
Globals extends AnyGlobal = AnyGlobal,
> = Map<string, Subcommand<Options, Globals, CommandArgPattern>>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyHandlerReturn = any | Promise<any>;

type GenericHandler<Options, Globals, Args> = (params: {
options: Options;
globals: Globals;
args: Args;
usage: string;
}) => void | Promise<void>;
}) => AnyHandlerReturn;

/**
* The settings for a subcommand.
Expand Down Expand Up @@ -138,14 +141,12 @@ export type HandlerParams<
Args extends string[] = never,
Globals extends AnyGlobal = never,
Usage extends string = never,
> = (
params: RemoveNever<{
options: Options;
args: Args;
globals: Globals;
usage: Usage;
}>,
) => void | Promise<void>;
> = RemoveNever<{
options: Options;
args: Args;
globals: Globals;
usage: Usage;
}>;

type RemoveNever<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
Expand Down
14 changes: 8 additions & 6 deletions test/e2e/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,12 @@ describe('docs', () => {
type Options = HandlerOptions<typeof options>;

// Handler that only needs options and globals
type DefaultHandler = HandlerParams<Options, never, Globals>;
type Params = HandlerParams<Options, never, Globals>;

const defaultHandler: DefaultHandler = ({ globals, options }) => {
const defaultHandler = ({ globals, options }: Params) => {
console.log({ globals, options });
// In a test, assert that true is returned
return true;
};

// Other possible options...
Expand All @@ -340,12 +342,12 @@ describe('docs', () => {
const noopHandler: NoParamsHandler = () => {};

// Positional arguments and options only
type HandlerWithArgs = HandlerParams<Options, [string]>;
const handlerWithArgs: HandlerWithArgs = ({ options, args }) => {};
type ParamsWithArgs = HandlerParams<Options, [string]>;
const handlerWithArgs = ({ options, args }: ParamsWithArgs) => {};

// Usage only
type HandlerWithUsage = HandlerParams<never, never, never, string>;
const handlerWithUsage: HandlerWithUsage = ({ usage }) => {};
type ParamsWithUsage = HandlerParams<never, never, never, string>;
const handlerWithUsage = ({ usage }: ParamsWithUsage) => {};

// No actual tests, just make sure this one compiles
expect(defaultHandler).toBeDefined();
Expand Down
12 changes: 4 additions & 8 deletions test/types/index.tst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,36 +101,32 @@ describe('subcommand modular external definition', () => {
}));

test('zero args', () => {
type Subcommand = HandlerParams;
type SubcommandParams = Parameters<Subcommand>[0];
type SubcommandParams = HandlerParams;

// eslint-disable-next-line @typescript-eslint/ban-types
expect<SubcommandParams>().type.toEqual<{}>();
});
test('with options', () => {
type Subcommand = HandlerParams<HandlerOptions<Options>>;
type SubcommandParams = Parameters<Subcommand>[0];
type SubcommandParams = HandlerParams<HandlerOptions<Options>>;

expect<SubcommandParams>().type.toEqual<{
options: HandlerOptions<Options>;
}>();
});
test('with globals and options', () => {
type Subcommand = HandlerParams<
type SubcommandParams = HandlerParams<
HandlerOptions<Options>,
never,
HandlerGlobals<Options>
>;
type SubcommandParams = Parameters<Subcommand>[0];

expect<SubcommandParams>().type.toEqual<{
options: HandlerOptions<Options>;
globals: HandlerGlobals<Options>;
}>();
});
test('with help', () => {
type Subcommand = HandlerParams<never, never, never, string>;
type SubcommandParams = Parameters<Subcommand>[0];
type SubcommandParams = HandlerParams<never, never, never, string>;

expect<SubcommandParams>().type.toEqual<{
usage: string;
Expand Down

0 comments on commit ff49af9

Please sign in to comment.