Classes 9 exercises

Class Implementation Following the Builder Pattern

One really cool aspect of classes is that they let us enact the builder pattern really beautifully. The builder pattern can be used to make some amazing APIs and it fits really nicely with TypeScript.

Here we have a class called BuilderTuple:

Loading explainer


0:00 One really cool aspect of classes is that they let us enact the builder pattern really beautifully. The builder pattern can be used to make some amazing APIs, and it fits really nicely with TypeScript. Let's look at what I mean.

0:15 We have a class here called BuilderTuple. If I hover over it, I can see that in the type argument, there's an empty tuple here. It's called builderBeforePush. Inside that builder, we have a list. That list is typed as an empty array. This is an empty tuple, nothing in it. What I can do is I can push things into the BuilderTuple. What it's going to do is it's going to add them to the array.

0:40 You've got builderBeforePush and then builderAfterPush. It's literally just pushing it onto the type level there. Push number 2, and we get builder 1, 2, and then push 3, and then we end up with BuilderTuple 1, 2, 3. The list after the push is 1, 2, and 3. That's what these tests are looking at here.

1:03 I'm going to just scrap all this and build it up from scratch so you can see how I constructed it. I'm going to export class, let's say, BuilderTuple. It's going to have a list on it. That list, it's going to be very, very dynamic. Anything can belong in this list. I'm going to make this a generic here. I'm going to make this TList and list is going to be TList.

1:29 Then when we build up the constructor, we're going to go this.list is an empty array. We've got our first error here, because TList could be instantiated with an arbitrary type that could be unrelated to never. Yes, you're right, never is not assignable to TList. We don't know what TList can be.

1:46 TList, a good constraint for it would be any array. It can be any array whatsoever. This.list equals blah, blah, blah, blah, blah is not assignable to the constraint again, so I'm going to patch it with as any array, or rather this one needs to go there. This.list equals this as any. Are you going to be happy with that? You'll be happy with that.

2:10 You'll notice with the builder pattern, you're going to need to use some as, some assertions here. Now we've got our initial BuilderTuple, but our tests are failing because the initial builder, it's typed as BuilderTuple any array. This list is now an array of any instead of being just a single tuple.

2:32 The way that we can do that, or rather a tuple without anything in, is we can add it into the default parameter here or the default generic. We can say equals this. This is really crucial. We need to have a specific point in the builder pattern where we start. This default generic gives it a point to start from. This list is now passing.

2:55 Now we need to implement the push method, because the push currently doesn't exist on here. Let's go push. What we're going to be pushing, we're going to be pushing, let's say, a number. This number, let's say, is just number. We're going to go this.list.push num. Then we're going to return this.

3:17 Now returning this, what this means is if we didn't do this, then the first instance would just return void, and we wouldn't be able to chain these methods together. By returning this, we get to go push, push, push, and build up all of this stuff. This is what's classically known as the builder pattern. You're building up a tuple, in this case.

3:38 There's an issue, which is the listAfterPush is still the same as the listBeforePush. While we've changed the list here, we haven't changed the generic in order to make it catch up. That's what we want to do. The way we can do them is we first need to infer the type from this push. We'll go TNum, and we'll say, "Let's put this in TNum here."

4:04 There's a small problem with this, which is that push currently just infers it as a number. We can constrain that a bit more by saying TNum extends number. Now it's going to infer it as 1, as 2, and as 3. That's really good. We've got our inference happening there and TNum is typed correctly.

4:23 The return type is not typed correctly. What we're trying to do is every time you push to it, we want to change this to append the new value to it. The way we do that is we can do...There's couple of ways. We can do this as BuilderTuple. Here, we're saying, "OK, we're doing this as BuilderTuple," taking advantage of the fact that you can pass classes as types.

4:49 We're going to say, "We're now going to pass in the new list that's here." Let's say we pass in this. Let's say we just pass in 1, this as BuilderTuple 1. This blah, blah, blah, blah, blah, blah, I'm going to have to do this as any as this for now, just to make it happy.

5:12 Now what we get is builderBeforePush is going to BuilderTuple with an empty array, and builderAfterPush is going to be BuilderTuple with a single member in it, which is this 1 that I've added. I don't want 1, I want this TNum. This TNum, we know it's going to be inferred as 1 here, inferred as 2 there, inferred as 3 there.

5:32 Except now, the builderAfterPush, you notice that the latest one gets pushed in. We can remove this, for instance, and then we get 2 being added in. The final flourish for this is we need to spread in the existing list and push TNum on to the end there. Now every time you push to it, it creates a new version of it.

5:58 This one, after this first push, we add the 1 in there. After the second push, we add the 2 in there. After the third push, we add the 3 in there. You end up with an actual tuple, which is inferred as all the members at runtime and on the type level.

6:14 Let's run back through all of that. We have a list. That list has to be instantiated with a default generic. There are a couple of as anys here to patch over some of the logic with TypeScript. We add a push method. That push method infers the new value on the type level. At runtime, it pushes it to the list.

6:34 Then we say, "Return this as any as BuilderTuple." A cleaner way to do this just to avoid a second as would be to make this the return type here. Let's do just one more fun thing, which is I'm going to add an unshift method. This, we'll add it to the start. This is going to call the unshift method on the list array here.

6:54 On here, instead of having TList and TNum -- See if you can pause the video briefly, see if you can figure this out for yourself -- we're going to swap the order of these. We'll have TNum at the start and TList at the end.

7:09 Now let's change the order of these a little bit. Let's say we unshift 3, unshift 2, unshift 1. Now, because we've done 3, 2, 1, it's going to reverse the order for us. It turns out to be exactly the same type as when we push them together but in a different order.

7:30 This is the builder pattern. It's incredibly cool because you can really deeply infer the things that you're adding into your root element, your root type, and you can build up amazing stuff with this.