Skip to content

Commit 660e855

Browse files
authored
Merge pull request #44 from Andarist/improve-autocompletion
Improve TS autocompletion for the type option in `bind` and `bindAll`
2 parents ec5d02c + 918cc5b commit 660e855

File tree

3 files changed

+76
-273
lines changed

3 files changed

+76
-273
lines changed

src/bind-all.ts

Lines changed: 20 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Binding, UnbindFn } from './types';
1+
import { Binding, InferEvent, InferEventType, Listener, UnbindFn } from './types';
22
import { bind } from './bind';
33

44
function toOptions(value?: boolean | AddEventListenerOptions): AddEventListenerOptions | undefined {
@@ -35,254 +35,31 @@ function getBinding(original: Binding, sharedOptions?: boolean | AddEventListene
3535
return binding;
3636
}
3737

38-
export function bindAll<Target extends EventTarget, Type extends string>(
39-
target: Target,
40-
bindings: [Binding<Target, Type>],
41-
sharedOptions?: boolean | AddEventListenerOptions,
42-
): UnbindFn;
43-
export function bindAll<Target extends EventTarget, Type1 extends string, Type2 extends string>(
44-
target: Target,
45-
bindings: [Binding<Target, Type1>, Binding<Target, Type2>],
46-
sharedOptions?: boolean | AddEventListenerOptions,
47-
): UnbindFn;
48-
export function bindAll<
49-
Target extends EventTarget,
50-
Type1 extends string,
51-
Type2 extends string,
52-
Type3 extends string,
53-
>(
54-
target: Target,
55-
bindings: [Binding<Target, Type1>, Binding<Target, Type2>, Binding<Target, Type3>],
56-
sharedOptions?: boolean | AddEventListenerOptions,
57-
): UnbindFn;
58-
export function bindAll<
59-
Target extends EventTarget,
60-
Type1 extends string,
61-
Type2 extends string,
62-
Type3 extends string,
63-
Type4 extends string,
64-
>(
65-
target: Target,
66-
bindings: [
67-
Binding<Target, Type1>,
68-
Binding<Target, Type2>,
69-
Binding<Target, Type3>,
70-
Binding<Target, Type4>,
71-
],
72-
sharedOptions?: boolean | AddEventListenerOptions,
73-
): UnbindFn;
74-
export function bindAll<
75-
Target extends EventTarget,
76-
Type1 extends string,
77-
Type2 extends string,
78-
Type3 extends string,
79-
Type4 extends string,
80-
Type5 extends string,
81-
>(
82-
target: Target,
83-
bindings: [
84-
Binding<Target, Type1>,
85-
Binding<Target, Type2>,
86-
Binding<Target, Type3>,
87-
Binding<Target, Type4>,
88-
Binding<Target, Type5>,
89-
],
90-
sharedOptions?: boolean | AddEventListenerOptions,
91-
): UnbindFn;
92-
export function bindAll<
93-
Target extends EventTarget,
94-
Type1 extends string,
95-
Type2 extends string,
96-
Type3 extends string,
97-
Type4 extends string,
98-
Type5 extends string,
99-
Type6 extends string,
100-
>(
101-
target: Target,
102-
bindings: [
103-
Binding<Target, Type1>,
104-
Binding<Target, Type2>,
105-
Binding<Target, Type3>,
106-
Binding<Target, Type4>,
107-
Binding<Target, Type5>,
108-
Binding<Target, Type6>,
109-
],
110-
sharedOptions?: boolean | AddEventListenerOptions,
111-
): UnbindFn;
11238
export function bindAll<
113-
Target extends EventTarget,
114-
Type1 extends string,
115-
Type2 extends string,
116-
Type3 extends string,
117-
Type4 extends string,
118-
Type5 extends string,
119-
Type6 extends string,
120-
Type7 extends string,
39+
TTarget extends EventTarget,
40+
TTypes extends ReadonlyArray<InferEventType<TTarget> & string>,
12141
>(
122-
target: Target,
42+
target: TTarget,
12343
bindings: [
124-
Binding<Target, Type1>,
125-
Binding<Target, Type2>,
126-
Binding<Target, Type3>,
127-
Binding<Target, Type4>,
128-
Binding<Target, Type5>,
129-
Binding<Target, Type6>,
130-
Binding<Target, Type7>,
44+
...{
45+
[K in keyof TTypes]: {
46+
type: TTypes[K] | (string & {});
47+
listener: Listener<
48+
TTarget,
49+
InferEvent<
50+
TTarget,
51+
// `& string` "cast" is not needed since TS 4.7 (but the repo is using TS 4.6 atm)
52+
TTypes[K] & string
53+
>
54+
>;
55+
options?: boolean | AddEventListenerOptions;
56+
};
57+
}
13158
],
13259
sharedOptions?: boolean | AddEventListenerOptions,
133-
): UnbindFn;
134-
export function bindAll<
135-
Target extends EventTarget,
136-
Type1 extends string,
137-
Type2 extends string,
138-
Type3 extends string,
139-
Type4 extends string,
140-
Type5 extends string,
141-
Type6 extends string,
142-
Type7 extends string,
143-
Type8 extends string,
144-
>(
145-
target: Target,
146-
bindings: [
147-
Binding<Target, Type1>,
148-
Binding<Target, Type2>,
149-
Binding<Target, Type3>,
150-
Binding<Target, Type4>,
151-
Binding<Target, Type5>,
152-
Binding<Target, Type6>,
153-
Binding<Target, Type7>,
154-
Binding<Target, Type8>,
155-
],
156-
sharedOptions?: boolean | AddEventListenerOptions,
157-
): UnbindFn;
158-
export function bindAll<
159-
Target extends EventTarget,
160-
Type1 extends string,
161-
Type2 extends string,
162-
Type3 extends string,
163-
Type4 extends string,
164-
Type5 extends string,
165-
Type6 extends string,
166-
Type7 extends string,
167-
Type8 extends string,
168-
Type9 extends string,
169-
>(
170-
target: Target,
171-
bindings: [
172-
Binding<Target, Type1>,
173-
Binding<Target, Type2>,
174-
Binding<Target, Type3>,
175-
Binding<Target, Type4>,
176-
Binding<Target, Type5>,
177-
Binding<Target, Type6>,
178-
Binding<Target, Type7>,
179-
Binding<Target, Type8>,
180-
Binding<Target, Type9>,
181-
],
182-
sharedOptions?: boolean | AddEventListenerOptions,
183-
): UnbindFn;
184-
export function bindAll<
185-
Target extends EventTarget,
186-
Type1 extends string,
187-
Type2 extends string,
188-
Type3 extends string,
189-
Type4 extends string,
190-
Type5 extends string,
191-
Type6 extends string,
192-
Type7 extends string,
193-
Type8 extends string,
194-
Type9 extends string,
195-
Type10 extends string,
196-
>(
197-
target: Target,
198-
bindings: [
199-
Binding<Target, Type1>,
200-
Binding<Target, Type2>,
201-
Binding<Target, Type3>,
202-
Binding<Target, Type4>,
203-
Binding<Target, Type5>,
204-
Binding<Target, Type6>,
205-
Binding<Target, Type7>,
206-
Binding<Target, Type8>,
207-
Binding<Target, Type9>,
208-
Binding<Target, Type10>,
209-
],
210-
sharedOptions?: boolean | AddEventListenerOptions,
211-
): UnbindFn;
212-
export function bindAll<
213-
Target extends EventTarget,
214-
Type1 extends string,
215-
Type2 extends string,
216-
Type3 extends string,
217-
Type4 extends string,
218-
Type5 extends string,
219-
Type6 extends string,
220-
Type7 extends string,
221-
Type8 extends string,
222-
Type9 extends string,
223-
Type10 extends string,
224-
Type11 extends string,
225-
>(
226-
target: Target,
227-
bindings: [
228-
Binding<Target, Type1>,
229-
Binding<Target, Type2>,
230-
Binding<Target, Type3>,
231-
Binding<Target, Type4>,
232-
Binding<Target, Type5>,
233-
Binding<Target, Type6>,
234-
Binding<Target, Type7>,
235-
Binding<Target, Type8>,
236-
Binding<Target, Type9>,
237-
Binding<Target, Type10>,
238-
Binding<Target, Type11>,
239-
],
240-
sharedOptions?: boolean | AddEventListenerOptions,
241-
): UnbindFn;
242-
export function bindAll<
243-
Target extends EventTarget,
244-
Type1 extends string,
245-
Type2 extends string,
246-
Type3 extends string,
247-
Type4 extends string,
248-
Type5 extends string,
249-
Type6 extends string,
250-
Type7 extends string,
251-
Type8 extends string,
252-
Type9 extends string,
253-
Type10 extends string,
254-
Type11 extends string,
255-
Type12 extends string,
256-
>(
257-
target: Target,
258-
bindings: [
259-
Binding<Target, Type1>,
260-
Binding<Target, Type2>,
261-
Binding<Target, Type3>,
262-
Binding<Target, Type4>,
263-
Binding<Target, Type5>,
264-
Binding<Target, Type6>,
265-
Binding<Target, Type7>,
266-
Binding<Target, Type8>,
267-
Binding<Target, Type9>,
268-
Binding<Target, Type10>,
269-
Binding<Target, Type11>,
270-
Binding<Target, Type12>,
271-
],
272-
sharedOptions?: boolean | AddEventListenerOptions,
273-
): UnbindFn;
274-
export function bindAll(
275-
target: EventTarget,
276-
bindings: Binding[],
277-
sharedOptions?: boolean | AddEventListenerOptions,
278-
): UnbindFn;
279-
export function bindAll(
280-
target: EventTarget,
281-
bindings: Binding[],
282-
sharedOptions?: boolean | AddEventListenerOptions,
28360
): UnbindFn {
284-
const unbinds: UnbindFn[] = bindings.map((original: Binding) => {
285-
const binding: Binding = getBinding(original, sharedOptions);
61+
const unbinds: UnbindFn[] = bindings.map((original) => {
62+
const binding: Binding = getBinding(original as never, sharedOptions);
28663
return bind(target, binding);
28764
});
28865

src/bind.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1-
import { UnbindFn, Binding } from './types';
1+
import { UnbindFn, InferEventType, InferEvent, Listener } from './types';
22

3-
export function bind<Target extends EventTarget, Type extends string>(
4-
target: Target,
5-
{ type, listener, options }: Binding<Target, Type>,
6-
): UnbindFn;
7-
export function bind(target: EventTarget, { type, listener, options }: Binding) {
3+
export function bind<TTarget extends EventTarget, TType extends InferEventType<TTarget> & string>(
4+
target: TTarget,
5+
// binding: Binding<
6+
// TTarget,
7+
// // `| (string & {})` should be moved to the Type's constraint
8+
// // however, doing that today breaks autocompletion
9+
// // this is being by https://github.com/microsoft/TypeScript/pull/51770 but we need wait for its release in TS 5.0
10+
// TType | (string & {})
11+
// >
12+
13+
// 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)
14+
// 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
15+
// most likely we'll be able to refactor this when https://github.com/microsoft/TypeScript/pull/51770 gets released in TS 5.0
16+
{
17+
type,
18+
listener,
19+
options,
20+
}: {
21+
// `| (string & {})` should be moved to the Type's constraint
22+
// however, doing that today breaks autocompletion
23+
// this is being fixed by https://github.com/microsoft/TypeScript/pull/51770 but we need wait for its release in TS 5.0
24+
type: TType | (string & {});
25+
listener: Listener<TTarget, InferEvent<TTarget, TType>>;
26+
options?: boolean | AddEventListenerOptions;
27+
},
28+
): UnbindFn {
829
target.addEventListener(type, listener, options);
930

1031
return function unbind() {

src/types.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
11
export type UnbindFn = () => void;
22

3-
type ExtractEventTypeFromHandler<MaybeFn extends unknown> = MaybeFn extends (
4-
this: any,
5-
event: infer MaybeEvent,
6-
) => any
7-
? MaybeEvent extends Event
8-
? MaybeEvent
9-
: Event
3+
type UnknownFunction = (...args: any[]) => any;
4+
5+
export type InferEventType<TTarget> = TTarget extends {
6+
// we infer from 2 overloads which are super common for event targets in the DOM lib
7+
// we "prioritize" the first one as the first one is always more specific
8+
addEventListener(type: infer P, ...args: any): void;
9+
// we can ignore the second one as it's usually just a fallback that allows bare `string` here
10+
// we use `infer P2` over `any` as we really don't care about this type value
11+
// and we don't want to accidentally fail a type assignability check, remember that `any` isn't assignable to `never`
12+
addEventListener(type: infer P2, ...args: any): void;
13+
}
14+
? P
1015
: never;
1116

12-
// Given an EventTarget and an EventName - return the event type (eg `MouseEvent`)
13-
// Rather than switching on every time of EventTarget and looking up the appropriate `EventMap`
14-
// We are being sneaky an pulling the type out of any `on${EventName}` property
15-
// This is surprisingly robust
16-
type GetEventType<
17-
Target extends EventTarget,
18-
EventName extends string,
19-
> = `on${EventName}` extends keyof Target
20-
? ExtractEventTypeFromHandler<Target[`on${EventName}`]>
21-
: Event;
17+
export type InferEvent<TTarget, TType extends string> =
18+
// we check if the inferred Type is the same as its defined constraint
19+
// if it's the same then we've failed to infer concrete value
20+
// it means that a string outside of the autocompletable values has been used
21+
// we'll be able to drop this check when https://github.com/microsoft/TypeScript/pull/51770 gets released in TS 5.0
22+
InferEventType<TTarget> extends TType
23+
? Event
24+
: `on${TType}` extends keyof TTarget
25+
? Parameters<Extract<TTarget[`on${TType}`], UnknownFunction>>[0]
26+
: Event;
2227

2328
// For listener objects, the handleEvent function has the object as the `this` binding
2429
type ListenerObject<TEvent extends Event> = {
25-
handleEvent(this: ListenerObject<TEvent>, e: TEvent): void;
30+
handleEvent(this: ListenerObject<TEvent>, event: TEvent): void;
2631
};
2732

2833
// event listeners can be an object or a function
29-
export type Listener<Target extends EventTarget, EventName extends string> =
30-
| ListenerObject<GetEventType<Target, EventName>>
31-
| { (this: Target, e: GetEventType<Target, EventName>): void };
34+
export type Listener<TTarget extends EventTarget, TEvent extends Event> =
35+
| ListenerObject<TEvent>
36+
| { (this: TTarget, ev: TEvent): void };
3237

33-
export type Binding<Target extends EventTarget = EventTarget, EventName extends string = string> = {
34-
type: EventName;
35-
listener: Listener<Target, EventName>;
38+
export type Binding<TTarget extends EventTarget = EventTarget, TType extends string = string> = {
39+
type: TType;
40+
listener: Listener<TTarget, InferEvent<TTarget, TType>>;
3641
options?: boolean | AddEventListenerOptions;
3742
};

0 commit comments

Comments
 (0)