From 278fcbe9f45be3139a0632c0f1db3c95a2684841 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 01/14] trying something new --- src/types.ts | 68 ++++++++++++++++++--------- test/bind-event-listener/bind.spec.ts | 11 +++++ test/type-tests/binding.type-test.ts | 12 ++--- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/types.ts b/src/types.ts index f245f9a..30d945e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,37 +2,63 @@ export type UnbindFn = () => void; type AnyFunction = (...args: any[]) => any; -type GetEventType = 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 - > - : Event +// type TargetMap = { +// [Window]: true +// } + +type ExtractEventTypeFromHandler = MaybeFn extends ( + this: any, + event: infer MaybeEvent, +) => any + ? MaybeEvent extends Event + ? MaybeEvent + : never : never; -type GetEventTypeFromListener = T extends (this: any, event: infer U) => any - ? U extends Event - ? U - : Event - : Event; +type GetEventType = + // case 1: does the target have a on${eventName} property? + `on${EventName}` extends keyof Target + ? ExtractEventTypeFromHandler + : Event; + +// case 3: is the `target` the `Document`? +// Target extends Document ? EventName extends keyof DocumentEventMap ? DocumentEventMap[EventName] : never : never +// : never; + +// type GetEventType = 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 +// > +// : Event +// : never; + +// type GetEventTypeFromListener = Fn extends ( +// this: any, +// event: infer U, +// ) => any +// ? U extends Event +// ? U +// : Event +// : Event; -export type Binding = { - type: Type; - listener: Listener, Target>; +export type Binding = { + type: EventName; + listener: Listener, Target>; options?: boolean | AddEventListenerOptions; }; -export type Listener = - | ListenerObject +export type Listener = + | ListenerObject // 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']; + | { bivarianceHack(this: Target, e: TEvent): void }['bivarianceHack']; -type ListenerObject = { +type ListenerObject = { // For listener objects, the handleEvent function has the object as the `this` binding - handleEvent(this: ListenerObject, Ee: Ev): void; + handleEvent(this: ListenerObject, e: TEvent): void; }; diff --git a/test/bind-event-listener/bind.spec.ts b/test/bind-event-listener/bind.spec.ts index 6994d87..e1accf6 100644 --- a/test/bind-event-listener/bind.spec.ts +++ b/test/bind-event-listener/bind.spec.ts @@ -1,6 +1,17 @@ import { UnbindFn } from '../../src/types'; import { bind } from '../../src'; +it('test', () => { + const button: HTMLElement = document.createElement('button'); + + bind(button, { + type: 'click', + listener: (event: Event) => { + console.log('button', event); + }, + }); +}); + it('should bind a listener', () => { const button: HTMLElement = document.createElement('button'); const onClick = jest.fn(); diff --git a/test/type-tests/binding.type-test.ts b/test/type-tests/binding.type-test.ts index b515a6c..15ddae8 100644 --- a/test/type-tests/binding.type-test.ts +++ b/test/type-tests/binding.type-test.ts @@ -3,16 +3,16 @@ import { Binding } from '../../src'; import { Listener } from '../../src/types'; // correctly extracts event type from target -expectTypeOf>().toEqualTypeOf<{ - type: 'beforeunload'; - listener: Listener; +expectTypeOf>().toEqualTypeOf<{ + type: 'click'; + listener: Listener; options?: boolean | AddEventListenerOptions; }>(); // fallbacks to Event if handler isn't present on the target -expectTypeOf>().toEqualTypeOf<{ - type: 'DOMContentLoaded'; - listener: Listener; +expectTypeOf>().toEqualTypeOf<{ + type: 'my-custom-event'; + listener: Listener; options?: boolean | AddEventListenerOptions; }>(); From 26eb21cf12e4bcf446ac902de253062203be3acf Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 02/14] trying a new internal typing approach --- package.json | 1 + src/index.ts | 2 +- src/types.ts | 47 +------ test/bind-event-listener/bind.spec.ts | 11 -- test/type-tests/binding.type-test.ts | 176 ++++++++++++++++++++------ yarn.lock | 7 +- 6 files changed, 154 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index f4a6316..12d7545 100644 --- a/package.json +++ b/package.json @@ -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.5.5" }, diff --git a/src/index.ts b/src/index.ts index d7dc402..12a55da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export { bind } from './bind'; export { bindAll } from './bind-all'; -export { Binding, UnbindFn } from './types'; +export { Binding, Listener, UnbindFn } from './types'; diff --git a/src/types.ts b/src/types.ts index 30d945e..8119dda 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,64 +1,31 @@ export type UnbindFn = () => void; -type AnyFunction = (...args: any[]) => any; - -// type TargetMap = { -// [Window]: true -// } - type ExtractEventTypeFromHandler = MaybeFn extends ( this: any, event: infer MaybeEvent, ) => any ? MaybeEvent extends Event ? MaybeEvent - : never + : Event : never; type GetEventType = - // case 1: does the target have a on${eventName} property? + // Does the target have a on${eventName} property? `on${EventName}` extends keyof Target ? ExtractEventTypeFromHandler : Event; -// case 3: is the `target` the `Document`? -// Target extends Document ? EventName extends keyof DocumentEventMap ? DocumentEventMap[EventName] : never : never -// : never; - -// type GetEventType = 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 -// > -// : Event -// : never; - -// type GetEventTypeFromListener = Fn extends ( -// this: any, -// event: infer U, -// ) => any -// ? U extends Event -// ? U -// : Event -// : Event; - export type Binding = { type: EventName; - listener: Listener, Target>; + listener: Listener; options?: boolean | AddEventListenerOptions; }; -export type Listener = - | ListenerObject - // 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: TEvent): void }['bivarianceHack']; +export type Listener = + | ListenerObject> + | { (this: Target, e: GetEventType): void }; -type ListenerObject = { +export type ListenerObject = { // For listener objects, the handleEvent function has the object as the `this` binding handleEvent(this: ListenerObject, e: TEvent): void; }; diff --git a/test/bind-event-listener/bind.spec.ts b/test/bind-event-listener/bind.spec.ts index e1accf6..6994d87 100644 --- a/test/bind-event-listener/bind.spec.ts +++ b/test/bind-event-listener/bind.spec.ts @@ -1,17 +1,6 @@ import { UnbindFn } from '../../src/types'; import { bind } from '../../src'; -it('test', () => { - const button: HTMLElement = document.createElement('button'); - - bind(button, { - type: 'click', - listener: (event: Event) => { - console.log('button', event); - }, - }); -}); - it('should bind a listener', () => { const button: HTMLElement = document.createElement('button'); const onClick = jest.fn(); diff --git a/test/type-tests/binding.type-test.ts b/test/type-tests/binding.type-test.ts index 15ddae8..0d60480 100644 --- a/test/type-tests/binding.type-test.ts +++ b/test/type-tests/binding.type-test.ts @@ -1,38 +1,140 @@ import { expectTypeOf } from 'expect-type'; -import { Binding } from '../../src'; -import { Listener } from '../../src/types'; - -// correctly extracts event type from target -expectTypeOf>().toEqualTypeOf<{ - type: 'click'; - listener: Listener; - options?: boolean | AddEventListenerOptions; -}>(); - -// fallbacks to Event if handler isn't present on the target -expectTypeOf>().toEqualTypeOf<{ - type: 'my-custom-event'; - listener: Listener; - options?: boolean | AddEventListenerOptions; -}>(); - -// correctly works with union target -expectTypeOf>().toEqualTypeOf<{ - type: 'beforeunload'; - listener: Listener; - options?: boolean | AddEventListenerOptions; -}>(); - -// correctly works with union type -expectTypeOf>().toEqualTypeOf<{ - type: 'abort' | 'beforeunload'; - listener: Listener; - options?: boolean | AddEventListenerOptions; -}>(); - -// correctly works with union target and type -expectTypeOf>().toEqualTypeOf<{ - type: 'abort' | 'beforeunload'; - listener: Listener; - options?: boolean | AddEventListenerOptions; -}>(); +import { Binding, Listener } from '../../src'; +import { expectType, TypeOf } from 'ts-expect'; + +describe('simple', () => { + // correctly extracts event type from target (HTMLElement) + expectTypeOf>().toEqualTypeOf<{ + type: 'click'; + listener: Listener; + options?: boolean | AddEventListenerOptions; + }>(); + + type ClickListener = Listener; + + // `this` and `event` are set correctly + expectType void>>(true); + + // okay to use a more generic `event` type + expectType void>>(true); + expectType void>>(true); + expectType void>>(true); + expectType void>>(true); + + // okay to use more generic `this` type + expectType void>>(true); + expectType void>>(true); + expectType void>>(true); + + // not okay to use the wrong `this` type + expectType void>>(false); + + // not okay to use the wrong `event` type + expectType void>>(false); +}); + +describe('simple - window', () => { + // correctly extracts event type from target (Window) + expectTypeOf>().toEqualTypeOf<{ + type: 'click'; + listener: Listener; + options?: boolean | AddEventListenerOptions; + }>(); + + type ClickListener = Listener; + + // `this` and `event` are set correctly + expectType void>>(true); + + // okay to use a more generic `event` type + expectType void>>(true); + expectType void>>(true); + expectType void>>(true); + expectType void>>(true); + + // okay to use more generic `this` type + expectType void>>(true); + expectType void>>(true); + expectType void>>(true); + + // not okay to use the wrong `this` type + expectType void>>(false); + + // not okay to use the wrong `event` type + expectType void>>(false); +}); + +describe('fallbacks', () => { + expectTypeOf>().toEqualTypeOf<{ + type: 'DOMContentLoaded'; + listener: Listener; + options?: boolean | AddEventListenerOptions; + }>(); + + // fallback to `Event` if handler isn't present on the target + expectType, (this: Window, e: Event) => void>>(true); + + // cannot fall back to some other event type + expectType, (this: Window, e: UIEvent) => void>>( + false, + ); +}); + +describe('custom events', () => { + expectTypeOf>().toEqualTypeOf<{ + type: 'my-custom-event'; + listener: Listener; + options?: boolean | AddEventListenerOptions; + }>(); + + // event is cast as `Event` (how can we know it is a custom event!?) + expectType< + TypeOf, (this: HTMLElement, e: Event) => void> + >(true); + // Cannot cast to `CustomEvent` as we don't know what the custom events are!! + expectType< + TypeOf, (this: HTMLElement, e: CustomEvent) => void> + >(false); +}); + +describe('event target unions', () => { + // correctly works with union target + expectTypeOf>().toEqualTypeOf<{ + type: 'beforeunload'; + listener: Listener; + options?: boolean | AddEventListenerOptions; + }>(); + + expectType< + TypeOf< + Listener, + (this: Window | HTMLElement, e: BeforeUnloadEvent) => void + > + >(true); + + expectType< + TypeOf< + Listener, + (this: Window | HTMLElement, e: AnimationEvent) => void + > + >(false); +}); + +describe('event type unions', () => { + // correctly works with union target + + type OurListener = Listener; + + expectTypeOf>().toEqualTypeOf<{ + type: 'keydown' | 'click'; + listener: OurListener; + options?: boolean | AddEventListenerOptions; + }>(); + + expectType void>>( + true, + ); + + // too narrow + expectType void>>(false); +}); diff --git a/yarn.lock b/yarn.lock index 15e9e41..13afe2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1263,7 +1263,7 @@ exit@^0.1.2: expect-type@^0.13.0: version "0.13.0" - resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" + resolved "https://packages.atlassian.com/api/npm/npm-remote/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" integrity sha512-CclevazQfrqo8EvbLPmP7osnb1SZXkw47XPPvUUpeMz4HuGzDltE7CaIt3RLyT9UQrwVK/LDn+KVcC0hcgjgDg== expect@^27.4.6: @@ -2666,6 +2666,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +ts-expect@^1.3.0: + version "1.3.0" + resolved "https://packages.atlassian.com/api/npm/npm-remote/ts-expect/-/ts-expect-1.3.0.tgz#3f8d3966e0e22b5e2bb88337eb99db6816a4c1cf" + integrity sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ== + ts-jest@^27.1.3: version "27.1.3" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.3.tgz#1f723e7e74027c4da92c0ffbd73287e8af2b2957" From d55dec6878eee5af462b3a0dd42a99cd1247ea45 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 03/14] more tests for types --- test/type-tests/bind-all.usage-types.ts | 71 +++++++++++++++++++ test/type-tests/bind.usage-types.ts | 48 +++++++++++++ .../{binding.type-test.ts => type-test.ts} | 10 +-- 3 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 test/type-tests/bind-all.usage-types.ts create mode 100644 test/type-tests/bind.usage-types.ts rename test/type-tests/{binding.type-test.ts => type-test.ts} (95%) diff --git a/test/type-tests/bind-all.usage-types.ts b/test/type-tests/bind-all.usage-types.ts new file mode 100644 index 0000000..1c5950c --- /dev/null +++ b/test/type-tests/bind-all.usage-types.ts @@ -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, + }, + ]); +} diff --git a/test/type-tests/bind.usage-types.ts b/test/type-tests/bind.usage-types.ts new file mode 100644 index 0000000..cfed84e --- /dev/null +++ b/test/type-tests/bind.usage-types.ts @@ -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, + }); +} diff --git a/test/type-tests/binding.type-test.ts b/test/type-tests/type-test.ts similarity index 95% rename from test/type-tests/binding.type-test.ts rename to test/type-tests/type-test.ts index 0d60480..1df0323 100644 --- a/test/type-tests/binding.type-test.ts +++ b/test/type-tests/type-test.ts @@ -2,7 +2,7 @@ import { expectTypeOf } from 'expect-type'; import { Binding, Listener } from '../../src'; import { expectType, TypeOf } from 'ts-expect'; -describe('simple', () => { +it('should correct extract the event type', () => { // correctly extracts event type from target (HTMLElement) expectTypeOf>().toEqualTypeOf<{ type: 'click'; @@ -33,7 +33,7 @@ describe('simple', () => { expectType void>>(false); }); -describe('simple - window', () => { +it('should extract event types from the Window', () => { // correctly extracts event type from target (Window) expectTypeOf>().toEqualTypeOf<{ type: 'click'; @@ -80,7 +80,7 @@ describe('fallbacks', () => { ); }); -describe('custom events', () => { +it('should handle custom events', () => { expectTypeOf>().toEqualTypeOf<{ type: 'my-custom-event'; listener: Listener; @@ -97,7 +97,7 @@ describe('custom events', () => { >(false); }); -describe('event target unions', () => { +it('should allow unions in the Binding type', () => { // correctly works with union target expectTypeOf>().toEqualTypeOf<{ type: 'beforeunload'; @@ -120,7 +120,7 @@ describe('event target unions', () => { >(false); }); -describe('event type unions', () => { +it('should allow type name unions', () => { // correctly works with union target type OurListener = Listener; From 68632b9c5ab8af5f81687a96c2f56ec1abbadfbb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 04/14] renaming test --- test/type-tests/{type-test.ts => types.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/type-tests/{type-test.ts => types.test.ts} (100%) diff --git a/test/type-tests/type-test.ts b/test/type-tests/types.test.ts similarity index 100% rename from test/type-tests/type-test.ts rename to test/type-tests/types.test.ts From 1f0252a559b425c55de550f52a75288ed0834a79 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 05/14] updating yarn registry url --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 13afe2b..ca391f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1263,7 +1263,7 @@ exit@^0.1.2: expect-type@^0.13.0: version "0.13.0" - resolved "https://packages.atlassian.com/api/npm/npm-remote/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" integrity sha512-CclevazQfrqo8EvbLPmP7osnb1SZXkw47XPPvUUpeMz4HuGzDltE7CaIt3RLyT9UQrwVK/LDn+KVcC0hcgjgDg== expect@^27.4.6: @@ -2668,7 +2668,7 @@ tr46@^2.1.0: ts-expect@^1.3.0: version "1.3.0" - resolved "https://packages.atlassian.com/api/npm/npm-remote/ts-expect/-/ts-expect-1.3.0.tgz#3f8d3966e0e22b5e2bb88337eb99db6816a4c1cf" + resolved "https://registry.yarnpkg.com/ts-expect/-/ts-expect-1.3.0.tgz#3f8d3966e0e22b5e2bb88337eb99db6816a4c1cf" integrity sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ== ts-jest@^27.1.3: From ec50922a4a0b4f540f1aece004cbc0d6362d7cd8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 06/14] Bump typescript from 4.5.5 to 4.6.4 Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.5 to 4.6.4. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/compare/v4.5.5...v4.6.4) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f4a6316..fc39ec3 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "rimraf": "^3.0.2", "size-limit": "^7.0.5", "ts-jest": "^27.1.3", - "typescript": "^4.5.5" + "typescript": "^4.6.4" }, "dependencies": {} } diff --git a/yarn.lock b/yarn.lock index 15e9e41..860861a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2704,10 +2704,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +typescript@^4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== universalify@^0.1.2: version "0.1.2" From a8bc784a0dc5a843a19a75ffb507921680796994 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 07/14] renaming file --- test/type-tests/{types.test.ts => public-types-test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/type-tests/{types.test.ts => public-types-test.ts} (100%) diff --git a/test/type-tests/types.test.ts b/test/type-tests/public-types-test.ts similarity index 100% rename from test/type-tests/types.test.ts rename to test/type-tests/public-types-test.ts From 690c5f99ccca039a3483efc2c46d52f9e9fd6e73 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:03:50 +1000 Subject: [PATCH 08/14] moving to node 16 --- .github/workflows/bundle-size-check.yml | 4 ++-- .github/workflows/test.yml | 2 +- .github/workflows/validate.yml | 3 +++ .nvmrc | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bundle-size-check.yml b/.github/workflows/bundle-size-check.yml index 93e6ecd..b3ca2c4 100644 --- a/.github/workflows/bundle-size-check.yml +++ b/.github/workflows/bundle-size-check.yml @@ -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 }} \ No newline at end of file + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4043122..fb91d59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5b239d6..445fae3 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 diff --git a/.nvmrc b/.nvmrc index f4e6f1b..c818c7b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.18.0 \ No newline at end of file +16.15.0 \ No newline at end of file From 99fcdc23cd7d6e649237ebd3f200c92a2e9e5edb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:19:46 +1000 Subject: [PATCH 09/14] cleaning up public types test --- test/type-tests/public-types-test.ts | 32 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test/type-tests/public-types-test.ts b/test/type-tests/public-types-test.ts index 1df0323..d1a806d 100644 --- a/test/type-tests/public-types-test.ts +++ b/test/type-tests/public-types-test.ts @@ -2,7 +2,8 @@ import { expectTypeOf } from 'expect-type'; import { Binding, Listener } from '../../src'; import { expectType, TypeOf } from 'ts-expect'; -it('should correct extract the event type', () => { +// extracting event type +{ // correctly extracts event type from target (HTMLElement) expectTypeOf>().toEqualTypeOf<{ type: 'click'; @@ -31,9 +32,10 @@ it('should correct extract the event type', () => { // not okay to use the wrong `event` type expectType void>>(false); -}); +} -it('should extract event types from the Window', () => { +// extracting event types from the `window` +{ // correctly extracts event type from target (Window) expectTypeOf>().toEqualTypeOf<{ type: 'click'; @@ -62,9 +64,10 @@ it('should extract event types from the Window', () => { // not okay to use the wrong `event` type expectType void>>(false); -}); +} -describe('fallbacks', () => { +// fallbacks for when there is no `on${EventName}` property +{ expectTypeOf>().toEqualTypeOf<{ type: 'DOMContentLoaded'; listener: Listener; @@ -78,9 +81,10 @@ describe('fallbacks', () => { expectType, (this: Window, e: UIEvent) => void>>( false, ); -}); +} -it('should handle custom events', () => { +// custom events +{ expectTypeOf>().toEqualTypeOf<{ type: 'my-custom-event'; listener: Listener; @@ -95,9 +99,11 @@ it('should handle custom events', () => { expectType< TypeOf, (this: HTMLElement, e: CustomEvent) => void> >(false); -}); +} -it('should allow unions in the Binding type', () => { +// The `Binding` type should allow EventTarget unions +// No idea why we want this, but it exists 🙈 +{ // correctly works with union target expectTypeOf>().toEqualTypeOf<{ type: 'beforeunload'; @@ -118,9 +124,11 @@ it('should allow unions in the Binding type', () => { (this: Window | HTMLElement, e: AnimationEvent) => void > >(false); -}); +} -it('should allow type name unions', () => { +// The `Binding` type should allow EventName unions +// No idea why we want this, but it exists 🙈 +{ // correctly works with union target type OurListener = Listener; @@ -137,4 +145,4 @@ it('should allow type name unions', () => { // too narrow expectType void>>(false); -}); +} From 37aec31ae72862629c7391697a3a27d050b3959b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 15:33:50 +1000 Subject: [PATCH 10/14] v2.1.0-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 11d3dda..60410e2 100644 --- a/package.json +++ b/package.json @@ -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 ", From d2746c1d66fe1e6329956183273708400a49e4ba Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 4 May 2022 17:02:09 +1000 Subject: [PATCH 11/14] adding tests for obj listener types --- .gitignore | 4 ++- ...=> public-types-function-listener-test.ts} | 0 .../public-types-object-listener-test.ts | 33 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) rename test/type-tests/{public-types-test.ts => public-types-function-listener-test.ts} (100%) create mode 100644 test/type-tests/public-types-object-listener-test.ts diff --git a/.gitignore b/.gitignore index c4c9459..e8f48ee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ node_modules/ dist/ # yarn -yarn-error.log \ No newline at end of file +yarn-error.log + +.npmrc \ No newline at end of file diff --git a/test/type-tests/public-types-test.ts b/test/type-tests/public-types-function-listener-test.ts similarity index 100% rename from test/type-tests/public-types-test.ts rename to test/type-tests/public-types-function-listener-test.ts diff --git a/test/type-tests/public-types-object-listener-test.ts b/test/type-tests/public-types-object-listener-test.ts new file mode 100644 index 0000000..9878aad --- /dev/null +++ b/test/type-tests/public-types-object-listener-test.ts @@ -0,0 +1,33 @@ +import { expectTypeOf } from 'expect-type'; +import { Binding, Listener } from '../../src'; +import { expectType, TypeOf } from 'ts-expect'; + +// extracting event type +{ + // correctly extracts event type from target (HTMLElement) + expectTypeOf>().toEqualTypeOf<{ + type: 'click'; + listener: Listener; + options?: boolean | AddEventListenerOptions; + }>(); + + type ClickListener = Listener; + + // `this` and `event` are set correctly + type Obj1 = { + handleEvent: (this: Obj1, e: MouseEvent) => void; + }; + expectType>(true); + + // not okay to use the wrong `this` type + type Obj2 = { + handleEvent: (this: number, e: MouseEvent) => void; + }; + expectType>(false); + + // not okay to use the wrong `event` type + type Obj3 = { + handleEvent: (this: Obj3, e: AnimationEvent) => void; + }; + expectType>(false); +} From ab68daf257ba4a8df719384bf861a1df61d70add Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 5 May 2022 10:28:19 +1000 Subject: [PATCH 12/14] adding custom event stuff --- .../public-types-function-listener-test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/type-tests/public-types-function-listener-test.ts b/test/type-tests/public-types-function-listener-test.ts index d1a806d..f6e7500 100644 --- a/test/type-tests/public-types-function-listener-test.ts +++ b/test/type-tests/public-types-function-listener-test.ts @@ -2,6 +2,13 @@ import { expectTypeOf } from 'expect-type'; import { Binding, Listener } from '../../src'; import { expectType, TypeOf } from 'ts-expect'; +// This is how you can 'teach' TS about your custom event +declare global { + interface HTMLElement { + 'onmy-custom-registered-event': CustomEvent<{ foo: number }>; + } +} + // extracting event type { // correctly extracts event type from target (HTMLElement) @@ -95,10 +102,18 @@ import { expectType, TypeOf } from 'ts-expect'; expectType< TypeOf, (this: HTMLElement, e: Event) => void> >(true); - // Cannot cast to `CustomEvent` as we don't know what the custom events are!! + // Cannot safely cast to `CustomEvent` as we don't know what the custom events are!! expectType< TypeOf, (this: HTMLElement, e: CustomEvent) => void> >(false); + // Cannot cast to `CustomEvent` as we don't know what the custom events are!! + + expectType< + TypeOf< + Listener, + (this: HTMLElement, e: CustomEvent<{ foo: number }>) => void + > + >(true); } // The `Binding` type should allow EventTarget unions From a10a1161dbcfa57fc74f97e2179880569359189d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 5 May 2022 12:05:35 +1000 Subject: [PATCH 13/14] adding more description to types --- src/types.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/types.ts b/src/types.ts index 8119dda..3731431 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,11 +9,16 @@ type ExtractEventTypeFromHandler = MaybeFn extends ( : Event : never; -type GetEventType = - // Does the target have a on${eventName} property? - `on${EventName}` extends keyof Target - ? ExtractEventTypeFromHandler - : 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 + : Event; export type Binding = { type: EventName; @@ -21,11 +26,12 @@ export type Binding = { + handleEvent(this: ListenerObject, e: TEvent): void; +}; + +// event listeners can be an object or a function export type Listener = | ListenerObject> | { (this: Target, e: GetEventType): void }; - -export type ListenerObject = { - // For listener objects, the handleEvent function has the object as the `this` binding - handleEvent(this: ListenerObject, e: TEvent): void; -}; From 54141446c030216630c96fe6f0dd706a310bb7d2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 5 May 2022 14:19:30 +1000 Subject: [PATCH 14/14] nicer ordering of types --- src/types.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types.ts b/src/types.ts index 3731431..f1d36b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,12 +20,6 @@ type GetEventType< ? ExtractEventTypeFromHandler : Event; -export type Binding = { - type: EventName; - listener: Listener; - options?: boolean | AddEventListenerOptions; -}; - // For listener objects, the handleEvent function has the object as the `this` binding type ListenerObject = { handleEvent(this: ListenerObject, e: TEvent): void; @@ -35,3 +29,9 @@ type ListenerObject = { export type Listener = | ListenerObject> | { (this: Target, e: GetEventType): void }; + +export type Binding = { + type: EventName; + listener: Listener; + options?: boolean | AddEventListenerOptions; +};