diff --git a/src/bind-all.ts b/src/bind-all.ts index 8f4b6fa..0799362 100644 --- a/src/bind-all.ts +++ b/src/bind-all.ts @@ -1,4 +1,4 @@ -import { Binding, UnbindFn } from './types'; +import { Binding, InferEvent, InferEventType, Listener, UnbindFn } from './types'; import { bind } from './bind'; function toOptions(value?: boolean | AddEventListenerOptions): AddEventListenerOptions | undefined { @@ -35,254 +35,31 @@ function getBinding(original: Binding, sharedOptions?: boolean | AddEventListene return binding; } -export function bindAll( - target: Target, - bindings: [Binding], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll( - target: Target, - bindings: [Binding, Binding], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, ->( - target: Target, - bindings: [Binding, Binding, Binding], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, - Type7 extends string, + TTarget extends EventTarget, + TTypes extends ReadonlyArray & string>, >( - target: Target, + target: TTarget, bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, + ...{ + [K in keyof TTypes]: { + type: TTypes[K] | (string & {}); + listener: Listener< + TTarget, + InferEvent< + TTarget, + // `& string` "cast" is not needed since TS 4.7 (but the repo is using TS 4.6 atm) + TTypes[K] & string + > + >; + options?: boolean | AddEventListenerOptions; + }; + } ], sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, - Type7 extends string, - Type8 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, - Type7 extends string, - Type8 extends string, - Type9 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, - Type7 extends string, - Type8 extends string, - Type9 extends string, - Type10 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, - Type7 extends string, - Type8 extends string, - Type9 extends string, - Type10 extends string, - Type11 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll< - Target extends EventTarget, - Type1 extends string, - Type2 extends string, - Type3 extends string, - Type4 extends string, - Type5 extends string, - Type6 extends string, - Type7 extends string, - Type8 extends string, - Type9 extends string, - Type10 extends string, - Type11 extends string, - Type12 extends string, ->( - target: Target, - bindings: [ - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - Binding, - ], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll( - target: EventTarget, - bindings: Binding[], - sharedOptions?: boolean | AddEventListenerOptions, -): UnbindFn; -export function bindAll( - target: EventTarget, - bindings: Binding[], - sharedOptions?: boolean | AddEventListenerOptions, ): UnbindFn { - const unbinds: UnbindFn[] = bindings.map((original: Binding) => { - const binding: Binding = getBinding(original, sharedOptions); + const unbinds: UnbindFn[] = bindings.map((original) => { + const binding: Binding = getBinding(original as never, sharedOptions); return bind(target, binding); }); diff --git a/src/bind.ts b/src/bind.ts index 1fd3872..1b8bc1e 100644 --- a/src/bind.ts +++ b/src/bind.ts @@ -1,10 +1,31 @@ -import { UnbindFn, Binding } from './types'; +import { UnbindFn, InferEventType, InferEvent, Listener } from './types'; -export function bind( - target: Target, - { type, listener, options }: Binding, -): UnbindFn; -export function bind(target: EventTarget, { type, listener, options }: Binding) { +export function bind & string>( + target: TTarget, + // binding: Binding< + // TTarget, + // // `| (string & {})` should be moved to the Type's constraint + // // however, doing that today breaks autocompletion + // // this is being by https://github.com/microsoft/TypeScript/pull/51770 but we need wait for its release in TS 5.0 + // TType | (string & {}) + // > + + // this "inline" variant works better when it comes to limiting `InferEvent` to using the `TType` from the "outer scope" (bind's and not Binding's) + // we can still export Binding and it could be used by people if they with to. To aid inference we can keep this inline within `bind`'s signature + // most likely we'll be able to refactor this when https://github.com/microsoft/TypeScript/pull/51770 gets released in TS 5.0 + { + type, + listener, + options, + }: { + // `| (string & {})` should be moved to the Type's constraint + // however, doing that today breaks autocompletion + // this is being fixed by https://github.com/microsoft/TypeScript/pull/51770 but we need wait for its release in TS 5.0 + type: TType | (string & {}); + listener: Listener>; + options?: boolean | AddEventListenerOptions; + }, +): UnbindFn { target.addEventListener(type, listener, options); return function unbind() { diff --git a/src/types.ts b/src/types.ts index f1d36b2..6ead631 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,37 +1,42 @@ export type UnbindFn = () => void; -type ExtractEventTypeFromHandler = MaybeFn extends ( - this: any, - event: infer MaybeEvent, -) => any - ? MaybeEvent extends Event - ? MaybeEvent - : Event +type UnknownFunction = (...args: any[]) => any; + +export type InferEventType = TTarget extends { + // we infer from 2 overloads which are super common for event targets in the DOM lib + // we "prioritize" the first one as the first one is always more specific + addEventListener(type: infer P, ...args: any): void; + // we can ignore the second one as it's usually just a fallback that allows bare `string` here + // we use `infer P2` over `any` as we really don't care about this type value + // and we don't want to accidentally fail a type assignability check, remember that `any` isn't assignable to `never` + addEventListener(type: infer P2, ...args: any): void; +} + ? P : never; -// 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 InferEvent = + // we check if the inferred Type is the same as its defined constraint + // if it's the same then we've failed to infer concrete value + // it means that a string outside of the autocompletable values has been used + // we'll be able to drop this check when https://github.com/microsoft/TypeScript/pull/51770 gets released in TS 5.0 + InferEventType extends TType + ? Event + : `on${TType}` extends keyof TTarget + ? Parameters>[0] + : Event; // For listener objects, the handleEvent function has the object as the `this` binding type ListenerObject = { - handleEvent(this: ListenerObject, e: TEvent): void; + handleEvent(this: ListenerObject, event: TEvent): void; }; // event listeners can be an object or a function -export type Listener = - | ListenerObject> - | { (this: Target, e: GetEventType): void }; +export type Listener = + | ListenerObject + | { (this: TTarget, ev: TEvent): void }; -export type Binding = { - type: EventName; - listener: Listener; +export type Binding = { + type: TType; + listener: Listener>; options?: boolean | AddEventListenerOptions; };