-
Notifications
You must be signed in to change notification settings - Fork 134
/
06-nonNullQuerySelector.exercise.ts
333 lines (308 loc) ยท 9.48 KB
/
06-nonNullQuerySelector.exercise.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/**
* ๐งโ๐ป Below, we've got a function called nonNullQuerySelector,
* which wraps document.querySelector. In this app, we do a lot
* of document.querySelector, so having a wrapper which meant
* we didn't have to always check if element was defined was
* useful.
*
* ๐งโ๐ป But this line of code is erroring, and I'd love to know why.
*/
/**
* ๐ก This function looks pretty simple. We take in a tag, like
* 'html', '.class', or something else, and pass it to
* document.querySelector.
*/
export function nonNullQuerySelector(tag: string) {
const element = document.querySelector(tag);
/** ^ ๐
*
* ๐ We can see here that document.querySelector can return
* either Element or null.
*/
if (!element) {
throw new Error(`Element not found with tag: ${tag}`);
}
return element;
/** ^ ๐
*
* ๐ By erroring if element is null, the element will be typed
* as just Element. This means that users of this function
* don't need to check if it's null.
*/
}
function onLoad() {
const result = nonNullQuerySelector("body");
result.addEventListener("gamepadconnected", (e) => {
console.log(e.gamepad);
/** ^ ๐
*
* ๐งโ๐ป Here, we're adding a listener to the body for when
* a gamepad is connected, but the gamepad isn't being
* inferred as GamepadEvent.
*
* ๐ Hover over e. It's being inferred as Event, not
* GamepadEvent.
*/
});
}
/**
* ๐ก This is a good example of relatively simple types getting
* into trouble when attempting to interface with an external
* library. In this case, the external library is the built-in
* DOM types that ship with TypeScript.
*
* In order to use types from an external lib, it's important to
* get to know them. Time to dive in.
*/
/**
* ๐ฎ When you're not using many type annotations, it can be tricky
* to know how to do a go-to-definition.
*
* Try exploring the code, trying different go-to-definition's, until
* you end up in a file called lib.dom.d.ts.
*
* Solution #1
*/
/**
* ๐ก Wow, this is a big file. It's 18,323 lines long. Let's see if
* we can find what we're looking for.
*
* ๐ฎ Try and find references to "gamepadconnected" in the file. A
* simple 'find' will do.
*
* You should see five.
*
* 1: The first is a comment on the GamepadEvent. This is cool, might
* be useful later.
*
* 2: The second is a key on a WindowEventMap, which is an interface
* which extends GlobalEventHandlersEventMap and
* WindowEventHandlersEventMap.
*
* 3: The third is a key on WindowEventHandlersMap itself.
*
* 4/5: Both 4/5 are part of ongamepadconnected, which doesn't seem
* relevant.
*/
/**
* ๐ฎ Go back to GamepadEvent. You should see two things:
*
* The first is a declare var GamepadEvent. This puts GamepadEvent
* into the global scope, so that you can create your own GamepadEvents
* at runtime:
*/
const gpEvent = new GamepadEvent("gamepadconnected", {
gamepad: {} as Gamepad,
});
gpEvent.gamepad;
/** ^ ๐/๐ฎ
*
* ๐ As you can see here, .gamepad is a property on the GamepadEvent.
*
* ๐ฎ Do a go-to-definition on .gamepad.
*
* BAM, you're back in GamepadEvent again.
*/
/**
* ๐ก So, for some reason body.addEventListener('gamepadconnected')
* isn't inferring e as GamepadEvent. If it did, our error would be
* solved.
*
* ๐งโ๐ป We've tried running this code on the client, and everything
* works. I.e. e.gamepad is there when we console.log it, but not
* in the types.
*/
/**
* ๐ก Let's check that body.addEventListener('gamepadconnected')
* will even work.
*
* ๐ฎ Inside lib.dom.d.ts, try to search for references to
* .addEventListener.
*
* Holy crap, 444 results.
*
* Try scrolling through a few of them to see if you can spot
* any patterns. Discuss with your group the patterns that you're
* seeing. In the solution, I've written down three observations.
* See if you can get them all.
*
* Solution #2
*
*/
/**
* ๐ก The fact that the functions come in pairs is interesting -
* we'll get back to that in a minute.
*
* First, let's hone in on the fact that the element types
* appear to be named in a pattern. It makes sense, given
* what we're seeing, that HTMLBodyElement would be something
* we should pay attention to.
*
* ๐ฎ Try searching for HTMLBodyElement.
*
* 13 results. You should notice that they're split out into
* four spots:
*
* In HTMLBodyElementEventMap
*
* In the HTMLBodyElement interface
*
* In the declare var HTMLBodyElement
*
* In a HTMLElementTagNameMap
*
* ๐ก Crucially, we appear to have found an answer to our
* question: does the body element accept an event of
* "gamepadconnected"?
*
* ๐ต๏ธโโ๏ธ Time for some proper sleuthing. You now have all the
* clues to work this out. Does the body element accept an
* event of "gamepadconnected"?
*
* Solution #3
*/
/**
* ๐ก OK, knowing that gives us some solid information. We're
* a little closer to solving the mystery.
*
* But we still don't know why the addEventListeners were
* arranged in pairs. Time to learn about function overloads:
*
* https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads
*/
document.addEventListener("animationstart", (e) => {});
/** ^ ๐/๐ฎ
*
* ๐ Hovering over addEventListener, we can see that at the
* end of the tooltip there's a (+1 overload).
*
* This tells us that there is MORE THAN ONE type signature
* for this function.
*
* ๐ฎ By doing a go-to-definition on the .addEventListener,
* we can see which version of the type signature is being
* used.
*
* You can see that the code above is using the _first_
* of the two.
*
* ๐ Try changing animationstart above to a random string:
*
* document.addEventListener("awdawdhkawdjhbaw", (e) => {});
*
* ๐ฎ You'll now see that when you go-to-definition, we end
* up at the second of the two overloads.
*/
/**
* ๐ก The reason this switch is happening is because "animationstart"
* is a member of the DocumentEventMap.
*
* addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
* ^ ๐ฎ ^ ๐ต๏ธโโ๏ธ
*
* Because it's assignable to this function, this is the overload
* that gets used.
*
* When we use something that isn't a member of the DocumentEventMap,
* it tries the other overload - which is just "string":
*
* addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
*
* ๐ก There's an important idea here. Overloads are read TOP TO BOTTOM.
* That means you should always put your narrowest overloads above your
* wider ones.
*
* ๐ต๏ธโโ๏ธ Go into lib.dom.d.ts and swap over the two addEventListeners, so
* the second is at the top.
*
* ๐ Hover document.addEventListener.
*
* document.addEventListener("animationstart", (e) => {});
* ^ ๐ ^ ๐
*
* You should see that the type is now being inferred as string, and
* the event is no longer AnimationEvent, but just Event.
*
* ๐ Change it back before anyone notices!
*/
/**
* ๐ก OK, understanding overloads is the final piece of the puzzle.
* Let's now walk backwards through the code to see if we can figure
* out why e.gamepad is not being inferred.
*
* ๐ Hover over result.addEventListener.
*
* result.addEventListener("gamepadconnected", (e) => {
* ^ ๐/๐ฎ
*
* ๐ฎ Do a go-to-definition to figure out which overload it's using.
* It should be the 'string' one. Why is that? See if you can figure
* it out.
*
* Solution #4
*/
/**
* ๐ก We know that HTMLBodyElement accepts a gamepadconnected event,
* and Element appears not to.
*
* So now the question is - why is result inferred as Element, and
* not HTMLBodyElement?
*/
/**
* ๐ฎ Take a look at document.querySelector above:
*
* const element = document.querySelector(tag);
* ^ ๐ฎ
*
* And take a look at the example code below:
*/
const bodyElem = document.querySelector("body");
// ^ ๐ ^ ๐ฎ
/**
* ๐ต๏ธโโ๏ธ Using all your sleuthing and crystal-ball skills, answer this
* question: why does document.querySelector infer HTMLBodyElement
* here, but NOT in our nonNullQuerySelector function?
*
* Solution #5
*/
/**
* ๐ Add an overload to nonNullQuerySelector which matches
* document.querySelector.
*
* You'll need to use HTMLElementTagNameMap, and you'll need a
* generic.
*
* Docs: https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads
*
* Solution #6
*
* โ
Incredibly, the error disappeared.
*/
/**
* ๐ Hover result:
*
* const result = nonNullQuerySelector("body");
* ^ ๐
*
* The result is HTMLBodyElement! Which means that:
*
* console.log(e.gamepad);
* ^ ๐
*
* e is GamepadEvent! Which, as we know, has the gamepad
* property on it.
*/
/**
* ๐ก Sometimes, type errors are not as they appear. This one
* took us through function overloads, a deep dive into
* lib.dom.d.ts, and generics. It turned out that the fix
* was in a whole other section than the highlighted error.
*/
/**
* ๐ต๏ธโโ๏ธ Stretch goal 1: Add another overload which uses
* SVGElementTagNameMap to handle SVG elements, and test
* that it works using:
*
* const clipPathElement = nonNullQuerySelector('clipPath');
* ^ ๐ Should be SVGClipPathElement
*/