Skip to content

Commit

Permalink
Merge pull request #72 from alexreardon/trying-new-types
Browse files Browse the repository at this point in the history
new public Listener type
  • Loading branch information
alexreardon authored May 5, 2022
2 parents e005836 + 5414144 commit a36ac1e
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 72 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/bundle-size-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
node-version: '16'

# The size limit github action
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
node-version: '16'

- name: Restore dependency cache
uses: actions/cache@v2
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16'

- name: Restore dependency cache
uses: actions/cache@v2
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ node_modules/
dist/

# yarn
yarn-error.log
yarn-error.log

.npmrc
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
12.18.0
16.15.0
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bind-event-listener",
"version": "2.0.0",
"version": "2.1.0-beta.0",
"private": false,
"description": "Making binding and unbinding DOM events easier",
"author": "Alex Reardon <[email protected]>",
Expand Down Expand Up @@ -52,6 +52,7 @@
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"size-limit": "^7.0.5",
"ts-expect": "^1.3.0",
"ts-jest": "^27.1.3",
"typescript": "^4.6.4"
},
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { bind } from './bind';
export { bindAll } from './bind-all';
export { Binding, UnbindFn } from './types';
export { Binding, Listener, UnbindFn } from './types';
53 changes: 26 additions & 27 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
export type UnbindFn = () => void;

type AnyFunction = (...args: any[]) => any;

type GetEventType<Target extends EventTarget, Type extends string> = Target extends unknown
? `on${Type}` extends keyof Target
? GetEventTypeFromListener<
// remove types that aren't assignable to `AnyFunction`
// so that we don't end up with union like `MouseEvent | Event`
Extract<Target[`on${Type}`], AnyFunction>
>
type ExtractEventTypeFromHandler<MaybeFn extends unknown> = MaybeFn extends (
this: any,
event: infer MaybeEvent,
) => any
? MaybeEvent extends Event
? MaybeEvent
: Event
: never;

type GetEventTypeFromListener<T extends AnyFunction> = T extends (this: any, event: infer U) => any
? U extends Event
? U
: Event
// Given an EventTarget and an EventName - return the event type (eg `MouseEvent`)
// Rather than switching on every time of EventTarget and looking up the appropriate `EventMap`
// We are being sneaky an pulling the type out of any `on${EventName}` property
// This is surprisingly robust
type GetEventType<
Target extends EventTarget,
EventName extends string,
> = `on${EventName}` extends keyof Target
? ExtractEventTypeFromHandler<Target[`on${EventName}`]>
: Event;

export type Binding<Target extends EventTarget = EventTarget, Type extends string = string> = {
type: Type;
listener: Listener<GetEventType<Target, Type>, Target>;
options?: boolean | AddEventListenerOptions;
// For listener objects, the handleEvent function has the object as the `this` binding
type ListenerObject<TEvent extends Event> = {
handleEvent(this: ListenerObject<TEvent>, e: TEvent): void;
};

export type Listener<Ev extends Event, Target extends EventTarget> =
| ListenerObject<Ev>
// For a listener function, the `this` binding is the target the event listener is added to
// using bivariance hack here so if the user
// wants to narrow event type by hand TS
// won't give them an error
| { bivarianceHack(this: Target, e: Ev): void }['bivarianceHack'];
// event listeners can be an object or a function
export type Listener<Target extends EventTarget, EventName extends string> =
| ListenerObject<GetEventType<Target, EventName>>
| { (this: Target, e: GetEventType<Target, EventName>): void };

type ListenerObject<Ev extends Event> = {
// For listener objects, the handleEvent function has the object as the `this` binding
handleEvent(this: ListenerObject<Ev>, Ee: Ev): void;
export type Binding<Target extends EventTarget = EventTarget, EventName extends string = string> = {
type: EventName;
listener: Listener<Target, EventName>;
options?: boolean | AddEventListenerOptions;
};
71 changes: 71 additions & 0 deletions test/type-tests/bind-all.usage-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { bindAll } from '../../src';

// inline definitions
{
const button: HTMLElement = document.createElement('button');

bindAll(button, [
{
type: 'click',
listener(event: MouseEvent) {},
},
{
type: 'keydown',
listener(event: KeyboardEvent) {},
},
]);
}

// inferred types
{
const button: HTMLElement = document.createElement('button');

bindAll(button, [
{
type: 'click',
listener(event: MouseEvent) {
const value: number = event.button;
},
},
{
type: 'keydown',
listener(event: KeyboardEvent) {
const value: string = event.key;
},
},
]);
}

// hoisted definitions
{
const button: HTMLElement = document.createElement('button');

function click(event: MouseEvent) {}
function keydown(event: KeyboardEvent) {}

bindAll(button, [
{
type: 'click',
listener: click,
},
{
type: 'keydown',
listener: keydown,
},
]);
}

// hoisted incorrect definitions
{
const button: HTMLElement = document.createElement('button');

function listener(event: KeyboardEvent) {}

bindAll(button, [
{
type: 'click',
// @ts-expect-error
listener: listener,
},
]);
}
48 changes: 48 additions & 0 deletions test/type-tests/bind.usage-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { bind } from '../../src';

// inline definitions
{
const button: HTMLElement = document.createElement('button');

bind(button, {
type: 'click',
listener(event: MouseEvent) {},
});
}

// inferred types
{
const button: HTMLElement = document.createElement('button');

bind(button, {
type: 'click',
listener(event) {
const value: number = event.button;
},
});
}

// hoisted definitions
{
const button: HTMLElement = document.createElement('button');

function listener(event: MouseEvent) {}

bind(button, {
type: 'click',
listener: listener,
});
}

// hoisted incorrect definitions
{
const button: HTMLElement = document.createElement('button');

function listener(event: KeyboardEvent) {}

bind(button, {
type: 'click',
// @ts-expect-error
listener: listener,
});
}
38 changes: 0 additions & 38 deletions test/type-tests/binding.type-test.ts

This file was deleted.

Loading

0 comments on commit a36ac1e

Please sign in to comment.