Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TypeScript support for Collection #81

Merged
merged 5 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 91 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

### Singular hook

Recommended for [TypeScript](#typescript)

```js
// instantiate singular hook API
const hook = new Hook.Singular();
Expand Down Expand Up @@ -526,49 +524,113 @@ hookCollection.remove("save", validateRecord);

## TypeScript

This library contains type definitions for TypeScript. When you use TypeScript we highly recommend using the `Hook.Singular` constructor for your hooks as this allows you to pass along type information for the options object. For example:
This library contains type definitions for TypeScript.

### Type support for `Singular`:

```ts
import { Hook } from "before-after-hook";

import {Hook} from 'before-after-hook'
type TOptions = { foo: string }; // type for options
type TResult = { bar: number }; // type for result
type TError = Error; // type for error

interface Foo {
bar: string
num: number;
}
const hook = new Hook.Singular<TOptions, TResult, TError>();

const hook = new Hook.Singular<Foo>();
hook.before((options) => {
// `options.foo` has `string` type

hook.before(function (foo) {
// not allowed
options.foo = 42;

// typescript will complain about the following mutation attempts
foo.hello = 'world'
foo.bar = 123
// allowed
options.foo = "Forty-Two";
});

// yet this is valid
foo.bar = 'other-string'
foo.num = 123
})
const hookedMethod = hook(
(options) => {
// `options.foo` has `string` type

const foo = hook(function(foo) {
// handle `foo`
foo.bar = 'another-string'
}, {bar: 'random-string'})
// not allowed, because it does not satisfy the `R` type
return { foo: 42 };

// foo outputs
{
bar: 'another-string',
num: 123
}
// allowed
return { bar: 42 };
},
{ foo: "Forty-Two" }
);
```

You can choose not to pass the types for options, result or error. So, these are completely valid:

```ts
const hook = new Hook.Singular<O, R>();
const hook = new Hook.Singular<O>();
const hook = new Hook.Singular();
```

In these cases, the omitted types will implicitly be `any`.

### Type support for `Collection`:

`Collection` also has strict type support. You can use it like this:

```ts
import { Hook } from "before-after-hook";

type HooksType = {
add: {
Options: { type: string };
Result: { id: number };
Error: Error;
};
save: {
Options: { type: string };
Result: { id: number };
};
read: {
Options: { id: number; foo: number };
};
destroy: {
Options: { id: number; foo: string };
};
};

const hooks = new Hook.Collection<HooksType>();

hooks.before("destroy", (options) => {
// `options.id` has `number` type
});

hooks.error("add", (err, options) => {
// `options.type` has `string` type
// `err` is `instanceof Error`
});

hooks.error("save", (err, options) => {
// `options.type` has `string` type
// `err` has type `any`
});

hooks.after("save", (result, options) => {
// `options.type` has `string` type
// `result.id` has `number` type
});
```

You can choose not to pass the types altogether. In that case, everything will implicitly be `any`:

```ts
const hook = new Hook.Collection();
```

An alternative import:
Alternative imports:

```ts
import { Singular, Collection } from "before-after-hook";

const hook = new Singular<{ foo: string }>();
const hookCollection = new Collection();
const hook = new Singular();
const hooks = new Collection();
```

## Upgrading to 1.4
Expand Down
152 changes: 118 additions & 34 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,166 @@
type HookMethod<O, R> = (options: O) => R | Promise<R>
type HookMethod<Options, Result> = (
options: Options
) => Result | Promise<Result>

type BeforeHook<O> = (options: O) => void
type ErrorHook<O, E> = (error: E, options: O) => void
type AfterHook<O, R> = (result: R, options: O) => void
type WrapHook<O, R> = (
hookMethod: HookMethod<O, R>,
options: O
) => R | Promise<R>
type BeforeHook<Options> = (options: Options) => void
type ErrorHook<Options, Error> = (error: Error, options: Options) => void
type AfterHook<Options, Result> = (result: Result, options: Options) => void
type WrapHook<Options, Result> = (
hookMethod: HookMethod<Options, Result>,
options: Options
) => Result | Promise<Result>

type AnyHook<O, R, E> =
| BeforeHook<O>
| ErrorHook<O, E>
| AfterHook<O, R>
| WrapHook<O, R>
type AnyHook<Options, Result, Error> =
| BeforeHook<Options>
| ErrorHook<Options, Error>
| AfterHook<Options, Result>
| WrapHook<Options, Result>

export interface HookCollection {
type TypeStoreKeyLong = 'Options' | 'Result' | 'Error'
type TypeStoreKeyShort = 'O' | 'R' | 'E'
type TypeStore =
| ({ [key in TypeStoreKeyLong]?: any } &
{ [key in TypeStoreKeyShort]?: never })
| ({ [key in TypeStoreKeyLong]?: never } &
{ [key in TypeStoreKeyShort]?: any })
type GetType<
Store extends TypeStore,
LongKey extends TypeStoreKeyLong,
ShortKey extends TypeStoreKeyShort
> = LongKey extends keyof Store
? Store[LongKey]
: ShortKey extends keyof Store
? Store[ShortKey]
: any

export interface HookCollection<
HooksType extends Record<string, TypeStore> = Record<
string,
{ Options: any; Result: any; Error: any }
>,
HookName extends keyof HooksType = keyof HooksType
> {
/**
* Invoke before and after hooks
*/
(
name: string | string[],
hookMethod: HookMethod<any, any>,
options?: any
): Promise<any>
<Name extends HookName>(
name: Name | Name[],
hookMethod: HookMethod<
GetType<HooksType[Name], 'Options', 'O'>,
GetType<HooksType[Name], 'Result', 'R'>
>,
options?: GetType<HooksType[Name], 'Options', 'O'>
): Promise<GetType<HooksType[Name], 'Result', 'R'>>
/**
* Add `before` hook for given `name`
*/
before(name: string, beforeHook: BeforeHook<any>): void
before<Name extends HookName>(
name: Name,
beforeHook: BeforeHook<GetType<HooksType[Name], 'Options', 'O'>>
): void
/**
* Add `error` hook for given `name`
*/
error(name: string, errorHook: ErrorHook<any, any>): void
error<Name extends HookName>(
name: Name,
errorHook: ErrorHook<
GetType<HooksType[Name], 'Options', 'O'>,
GetType<HooksType[Name], 'Error', 'E'>
>
): void
/**
* Add `after` hook for given `name`
*/
after(name: string, afterHook: AfterHook<any, any>): void
after<Name extends HookName>(
name: Name,
afterHook: AfterHook<
GetType<HooksType[Name], 'Options', 'O'>,
GetType<HooksType[Name], 'Result', 'R'>
>
): void
/**
* Add `wrap` hook for given `name`
*/
wrap(name: string, wrapHook: WrapHook<any, any>): void
wrap<Name extends HookName>(
name: Name,
wrapHook: WrapHook<
GetType<HooksType[Name], 'Options', 'O'>,
GetType<HooksType[Name], 'Result', 'R'>
>
): void
/**
* Remove added hook for given `name`
*/
remove(name: string, hook: AnyHook<any, any, any>): void
remove<Name extends HookName>(
name: Name,
hook: AnyHook<
GetType<HooksType[Name], 'Options', 'O'>,
GetType<HooksType[Name], 'Result', 'R'>,
GetType<HooksType[Name], 'Error', 'E'>
>
): void
/**
* Public API
*/
api: Pick<
HookCollection<HooksType>,
'before' | 'error' | 'after' | 'wrap' | 'remove'
>
}

export interface HookSingular<O, R, E> {
export interface HookSingular<Options, Result, Error> {
/**
* Invoke before and after hooks
*/
(hookMethod: HookMethod<O, R>, options?: O): Promise<R>
(hookMethod: HookMethod<Options, Result>, options?: Options): Promise<Result>
/**
* Add `before` hook
*/
before(beforeHook: BeforeHook<O>): void
before(beforeHook: BeforeHook<Options>): void
/**
* Add `error` hook
*/
error(errorHook: ErrorHook<O, E>): void
error(errorHook: ErrorHook<Options, Error>): void
/**
* Add `after` hook
*/
after(afterHook: AfterHook<O, R>): void
after(afterHook: AfterHook<Options, Result>): void
/**
* Add `wrap` hook
*/
wrap(wrapHook: WrapHook<O, R>): void
wrap(wrapHook: WrapHook<Options, Result>): void
/**
* Remove added hook
*/
remove(hook: AnyHook<O, R, E>): void
remove(hook: AnyHook<Options, Result, Error>): void
/**
* Public API
*/
api: Pick<
HookSingular<Options, Result, Error>,
'before' | 'error' | 'after' | 'wrap' | 'remove'
>
}

type Collection = new () => HookCollection
type Singular = new <O = any, R = any, E = any>() => HookSingular<O, R, E>
type Collection = new <
HooksType extends Record<string, TypeStore> = Record<
string,
{ Options: any; Result: any; Error: any }
>
>() => HookCollection<HooksType>
type Singular = new <
Options = any,
Result = any,
Error = any
>() => HookSingular<Options, Result, Error>

interface Hook {
new (): HookCollection
new <
HooksType extends Record<string, TypeStore> = Record<
string,
{ Options: any; Result: any; Error: any }
>
>(): HookCollection<HooksType>

/**
* Creates a collection of hooks
Expand Down