Making TypeScript Stick Typing jQuery Solution
Transcript from the "Typing jQuery Solution" Lesson
>> Let's look at how to invent a jQuery on top of the thing that replaced jQuery, the DOM API. So, the first thing we're gonna do is navigate to the Project folder, which is, it's in the Challengers folder > jquery-clone. And most of our code is gonna be, actually all of our code should be in this index.ts folder.
[00:00:21] So, now the first thing that we're gonna see is kind of a harness starting point to make sure that at least we can export this dollar sign thing, that's a namespace piggybacking on top of a function. Cool thing to notice is it's actually important to have them ordered the way that they were ordered.
[00:00:41] Whenever the namespace is used to piggyback on top of a value of some sort, a function being a value, namespace has to come second, it can't come first. So this is fine. Okay, so the first thing I want to tackle is, in order, let's do the first snippet from the jQuery Getting Started page.
[00:01:04] So I'm just gonna copy this into my editor just so I can see it real easy, and we need to make, actually it better to have it done here. So I see type errors. We need to make sure that this somehow works. So the dollar sign, this is equivalent to document.querySelectorAll, right?
[00:01:24] So querySelector would have worked as well there. I don't think there are any examples here where the tests would fail for one element versus many, but let's use querySelectorAll. So, what we want is somehow to retain the result of a querySelector, so let's call this elements. Actually, yeah, let's make a class.
[00:01:58] SelectorResult, and we're gonna return a new SelectorResult. And this is gonna take in as an argument, querySelectorAll, and we'll pass in that selector string which would be like button.continue. And right now we're getting an error. Let's see, does our editor help us at all, give us a little balloon?
[00:02:23] Nope, it's not gonna do that for us. That's fine. So what we want is the return value here, NodeListOf, and then type is element. So we'll create a private field that's of this type. In fact, I wonder if this will work. Nope, I'd rather use the private field, I was debating whether I wanna use sort of the the terse code or the actually private thing.
[00:03:02] So we'll use the actual private thing. And we're gonna take advantage of the fact that the type of this field can be inferred now, thanks to us doing an assignment in our constructor. This is one of our new language features. So, what we get now is this function.
[00:03:25] Dollar sign should return, A SelectorResult, but why is it not doing that? Cuz I haven't explicitly typed as any, that would do it. So there we go, dollar sign returns a SelectorResult. Now, SelectorResult needs a method on it called html. And what should this do? This should set the contents, like inner HTML equals, that's what this is all about.
[00:03:54] So through using the little VS Code helper tooltip thing here, I just said, declare a method, html. And then up here, I get this thing, this method stub that throws whenever you call it. We'll say that's the content. And what we wanna do is iterate over the list of elements and anything that might be in the SelectorRresult, something that our DOM query brought back.
[00:04:20] All of those, we should apply our inner HTML equals and then contents. So this.#elements.forEach, there's a nice little for each on here. Something you may have noticed, this is not an array. NodeList is not necessarily an array. So, I don't think you can simply do that, no, I guess you can.
[00:04:48] I do remember that there are some things like, is there a map or a filter? Yeah, there's no higher order functions on it. So it's arrayish, but not quite an array. So forEach, and we'll call this an element. And it's possible to have elements that are not HTML elements.
[00:05:14] By that I mean this type here. So, Thankfully we don't need to worry about that, so we can just simply say, contents. Cool, so in essence what we're doing is iterate over everything we found, and then, Set the contents equal to the string we were given. Pretty simple, and now this may work.
[00:05:50] Let's run our test and see. I expect plenty of these to still fail. At least six passed, which is great. Okay, and this is the Ajax test that currently fails. Here's our event listener. Visibility hidden, great. So these have to do with .hide. But I see, sorry, for duplicate tests here, the duplicate test names.
[00:06:22] The purpose of these tests is different. But nothing that has to do with this .html thing. So, it returns something truthy and effectively sets that text to what it should be. That's great. So let's deal with hide. So this is going to be the second little example here, let's just copy that down here and paste.
[00:06:50] And of course, so this is our event listener thing that we need to fill out and then show. So hiddenBox, this is like a selector result. So that should have a show method, and this is also something on the SelectorResult. So I can go here and say Add all missing members, this will create those little step functions for us with kind of some semblance of argument types but nothing really too specific.
[00:07:23] I'm just gonna move those down here. Okay, so this is gonna be our event name. Now we could use this as a string. All the examples suggested that you do was a click, right? There's something a little bit more clever that you could do here, because there are only certain strings that map to DOM events.
[00:07:43] Can anyone guess where we might find these strings? Like if I were to say, there might be another, a DOM API or a type of something where you might find an event name. What's a good place we might look? Something modern that you might use today. Well, if I wanna, say I have a, Button element like this.
[00:08:13] How would I register a click listener with this button?
>> Add event listener.
>> Add event listener, so that's one thing we could do. So if we look at this, addEventListener, the first argument it takes is, it's like type K, so it's generic over this thing here, HTMLEventMap.
[00:08:40] And if we look at this, so it's got a couple different maps here but let's look at what those look like. Hey, look, we've got event names and then interfaces that represent the thing that our callback passed to addEventListener will receive. So here we've got like full screen change, things like that, GlobalEventHandlers, I mean, a whole bunch of good stuff there.
[00:09:06] But these are the strings that we're interested in, so it's HTMLEventMap. So, we could say, all right, I'm gonna leave that there cuz I think we'll end up using it. So, we'll make this, We're gonna make this a generic method. K is declared but never read. Yeah, that's fine.
[00:09:33] Sorry, K extends. There we go. So this first argument, eventName is gonna be, let me get rid of this, since we don't need it too much. It's gonna be a K. So this would be the string click. And then this event over here, let's bring it down to the second line.
[00:09:54] We're gonna use our map type, like that. So what does this mean? When we pass in click, look, we get a MouseEvent back. If we did key up, Look at that nice little autocomplete there, it's like everything's on the menu. Key down, we get a KeyboardEvent, so we get nice typing coming through, depending on what type of event we're registering, we get this well typed thing back, so that's great.
[00:10:30] A string would have been fine, but you would have had to pick your lowest common denominator of event type. And then it's sort of up to whoever's using this, they're gonna have to sort of sniff out, is this what I hoped it would be? And it gets a little more complicated.
[00:10:47] Great, so that's the eventName, here's the callback, call this just CB. And here's the callback argument, it's gonna pass along this actual event. Fantastic. So now, what we can do is, again, iterate over everything we found. This is like the result set. And, Add an event listener to each.
[00:11:24] EventName and callback. I feel like I'm missing a paren. Great, now what's the type error here? Clipboard data is declared here. AddEventListener, let's pass in this K explicitly. You know what? This is the problem we're running into. This base class here, addEventListener, we've got ElementEventMap here. Now if you recall, let me go back to our button case.
[00:12:13] Look at this. When I click on the type declaration for the button, we have HTMLElementEventMap. And then when I go back here, I've just got ElementEventMap. So there are these non-HTML elements that sort of have different events that they can deal with. And that's where I'm running into this incompatibility.
[00:12:39] So I have two options here. One is I could just say, all right, let's just go back to the ElementEventMap. Let's not make this the HTMLElementEventMap. Or I could filter as I'm looping over things and sort of sniff out whether they're matching that interface. And if they are, we proceed, if they're not, it's a no op.
[00:13:02] So I'm actually just gonna generalize this a little bit so we can get back to ElementEventMap, at which point this quiets down and everything looks to be happy. But does everyone understand kind of what we ran into there? addEventListener is more constrained when working with HTML elements because only a subset of events work with those, compared to the more general element class.
[00:13:27] Great, so that should work. Let's run the test see if any are passing. You know what? I also have some Rando code down here. I should probably comment that out. All right, so three tests failing, it was four before, so now we're down to three. And let's see, so there's our Ajax test.
[00:13:51] I didn't expect that to pass yet. Hide and visibility hidden, I didn't expect either of those to pass either. So I think that we might be done with the addEventListener portion of this. So now we need the sort of hide and, I mean, we could implement a show but let's just keep it simple.
[00:14:09] Click is not in the ElementEventMap. I'm gonna improvise the solution. So here, we would have to, I'm gonna do something quick and dirty here, but this is where we'd want a type guard to check whether something, Is an HTML element versus a element. In this case I'm gonna say it's probably not harmful to register extra event listeners to these things.
[00:14:58] I would hope they would not fire, but if this were real code I was shipping, I would do something a little bit more rigorous than that, than just a cast. So now nothing should be happy. So the thing that we fixed was click was not on that list of things that the base layer element supports.
[00:15:17] All right, show is what we're implementing. That's right up here. So again, looping over everything that we found. And we might need the same cast, style.visibility = visible, I believe is the thing we use. Yep, Three failed still. Looks like the test wants us to implement a hide as well.
[00:16:11] That's no problem, we can just copy this. Hide, and this'll be hidden or, yeah, hidden, perfect, should see fewer than three test failures now. One test failure left, and that's gonna be the Ajax situation. So we've got hide, and show, we can collapse those, kinda get those out of the way.
[00:16:38] There's our event listener thing, our HTML, like setting the contents of something. Finally, time for us to implement this thing here, right? So what do we want? We wanna take in, well, we can just grab our little code snippet here. We want something that allows us to pass in a URL and a success callback.
[00:17:01] And then this result appears to have a title and a body, maybe it's some JSON thing. Yeah, should be fairly straightforward. So, we've got fetch already imported up here, node-fetch, this is just cuz the test suite's running this in node, normally this would be in your browser API.
[00:17:25] So we'll return the promise that fetch resolves to, not a strict requirement of this challenge but it is the way Ajax worked, in fact. And we're gonna take as an argument a URL, A success callback. So fetch(url).then and then we get a response back, which we then have to decode into JSON.
[00:18:14] And then we can actually just pass our success callback in there. This has to be a string. It's just formatting that a little bit better. Let's see if the tests pass. Oop, still missing something. I misread this. I should be taking an object, a single configuration object, rather than arguments.
[00:18:50] So here's the type of our argument. Here's the actual destructured assignment that we're using in order to obtain into the values, and our tests pass. So we're just making the fetch, we're grabbing the response. So this gets called when the first byte lands in the browser. And then this promise resolves when the last byte lands in the browser.
[00:19:50] But you can apply types that describe this type of code.