Change language

Statically Prevent 404s – Gary Bernhardt

Statically Prevent 404s - Gary Bernhardt

- [Narrator] This is a talk called statically prevent 404s.

My name is Gary Bernhardt.

Lets begin by looking at a very hastily drawn diagram of what your web applications look like.

Ignore this for a second.

You have some routes to find with patterns with the familiar syntax, the colon param syntax.

And youve got surely links, A tags and other kinds of path references.

But in many, probably most applications, these two are kind of live in separate worlds.

Theres nothing tying them together and ensuring that your links actually links to things that are really routed somewhere.

So what were gonna do in this talk is see and then actually build from scratch a system that binds the two together, a system for formally defining what paths are in the system.

We then just throw those right into the router.

Totally trivial transformation there.

And when we link to those, theyre going to ensure that we actually have provided the required params and that the link actually exists.

All of that was pretty abstract.

So let me eradicate all this gruesome analog stuff and do the thing we came to do, which is look at a computer screen. (laughs) So this is the paths module of execute program.

You can see that its just exporting a bunch of names, each of which comes from calling this path function on a pattern.

All looks very sort of innocuous and normal.

Now before we see how any of this works and what its doing, let me show you this lesson path in action.

One of the places where we link that is on the review page.

This is our TypeScript course, which will interactively teach you TypeScript.

We have a whole bunch of lessons.

Then later you get reviews that review the things you learned in lessons.

And the thing I wanna show you here is not this particular code example, but if I finish it, this little line at the bottom here contains a link to the corresponding lesson.

If we open that, there it is.

Now we wanna make sure that this link works.

If we change the structure of a lesson path, we wanna make sure that this doesnt turn into a 404.

So were gonna use this little thing, this unobtrusive link that would be easy to forget about when updating, were gonna use this as our motivating example.

So going back to the definition of that path, suppose that we wanna add for some reason some new param here.

Maybe we version our lessons and so we have a version there.

If I write that change out, we are immediately gonna get a whole bunch of type errors.

There were no type errors before.

Now, theres one in the backend and there are seven in the front-end.

And these are the eight places in the system that are linking to a lesson page.

If we look at the message, argument of type courseId, lessonId is not assignable to parameter of type courseId lessonId version.

So we have a whole bunch of places where were giving these params, but theyre no longer the correct params and all of those are type errors.

Of course, likewise, if we just delete that path, were gonna get type errors because now the thing that is being, well, Webpack is also mad about that but the thing thats being imported doesnt even exist anymore.

So of course, thats a type error.

Now lets dig in and look at how we actually use these paths in the router and how we use them when were linking.

The router is actually the simpler case.

These paths are, theyre actually functions, but they also have properties.

One of the properties on our lesson path is going to be the pattern, which is just a normal rails slash express style pattern that you can throw right into react router as we are here.

React router has no idea that any of this is happening, but it still works just fine.

The other side we wanna look at is the actual component for this little line here.

This link right here is this A tag.

Now its red right now because we broke it by adding that version.

Lets fix it.

The language servers gonna take a moment to notice that.

The paths module is imported by everything.

So whenever we change it, especially for some reason in Vim, the language server takes a while to actually figure it out.

I think itll fix it eventually.

Anyway, Ill just kill it and restart it.

So we were linking to the path by just calling that function and the function is what we got by defining the path.

So this gives us a function.

We call it over here with the corresponding parameters.

Its really simple.

And we get out an actual path, that is the link that we clicked earlier.

So that is how we define paths.

Theyre trivial to use in the router.

Theres basically no extra complexity there and linking to them, I mean, maybe this is a little more complex than using a string literal or something, but its not that much more complex.

So how does this all work? (laughs) Thats the interesting question for TS Conf.

So lets just build the whole thing or at least the types from scratch.

Were not gonna build the runtime code because thats normal JavaScript stuff, its less interesting.

Well begin with.

We definitely want that path function and its gonna take a pattern.

The pattern is gonna have a type that is some kind of literal string type, that is the actual pattern we typed in.

So pattern extends string.

Then our function is going to return a path, and the path needs to know about the params.

So well add a helper type thats called params.

Its gonna take the pattern and this is gonna turn that pattern into the corresponding object type, and then the path is parametrized on that.

So get the syntax right and now lets define, theres a couple orders that we could do this, and lets start with the path type.

Its gonna take the params.

Its just a function that takes those as an argument and returns a string that is the generated URL, the generated path.

This is just a normal string because by the time weve really generated an actual concrete path, we no longer need any type information cause were just gonna throw it into an href attribute on an A tag.

Let me get rid of this error by just hitting the type system with any hammer. (laughs) We dont care about the body here.

The params type, this is where all the complexity here is gonna live.

Im actually gonna start not by defining this, but defining another type its gonna need.

So lets just totally think about this type in isolation.

This type is going to take an actual path specification, a pattern.

So something like our lesson pattern that weve already seen and its going to give us a union of literal string types that are the names of the parameters in that pattern.

So something like courseId or lessonId in our case, because those are the two holes here, the two params.

In most statically typed languages, this is not something you could do.

It looks impossible but it is totally doable.

No co-gen or anything.

Its only possible because of the new template literal type inference.

I think I said that right. (chuckles) Feature that came sometime in three 3.X.

I dont remember when.

So lets start actually writing that type.

Were gonna have the, what is this, well call this ParamNames because it gives us the names of the parameters from that pattern.

Its gonna take a pattern, which is some kind of string.

Incidentally, I should mention, because of the way were defining this, this could just be string or it could be a union of a bunch of strings.

Neither of those is intended usage of this.

If you do that, you will end up with a type error.

It may be confusing.

I mean, if you look at the way this is used, youre probably not gonna make that kind of mistake.

Anyway, starting with this type definition.

In fact, before we even start with this type definition, lets write a test of sorts.

So well say if we have some X that is ParamNames of just the string courseId with the colon.

So this is a tiny little route pattern.

We should be able to assign the string courseId to that.

So basically, were saying, its gonna chop off that colon.

Now right now its passing because any, but if we get rid of the any and put an actual type in, then well get something more interesting.

This is gonna be a three-way conditional type.

Its gonna use infer.

Its gonna have template literal type inference, template literal string type inference.

I forget the correct invocation.

Its gonna have that thing, you know what I mean.

So theres some pretty advanced types in here, but well start with the simple case that corresponds to this.

If pattern is a string that has the shape where it starts with the colon and then has anything after that, which well call the ParamName.

Then this just is that ParamName, so were just saying slice the colon off.

Otherwise, well get never, of course, everyones favorite base case.

And that makes this type check.

Now if I mess this up.

Now it doesnt type check.

We got a red underline here, thats lint.

There we go.

courseIda is not assignable to type courseId.

So this was the easy case.

Now we get a couple of cases that are a little bit harder.

What if pattern extends? Again, it starts with the colon.

Again, we have a param, but then we have a slash followed by some other stuff.

We dont really care what that is.

In that case, again, we wanna return the param, but we have to union it with whatever params are in here.

We dont know what this is.

This could be a very long string with lots of params in it.

So we will recursively invoke our type and well give it Rest.

I have a type error because colon.

So if we write a test for that.

Lets say we have, so this would be something like courseId/:lessonId and well give it a.

Is this gonna work? Yeah, thatll work.

Well make it in array type, so we can make an array of courseId, lessonId.

I believe that should work, yup.

If I typo lessonIda, no good.

Type error says not assignable to, and you can see at the bottom of my screen there, theres that union.

So we are getting a union of those two things.

Now the third case is the one that lets us fully generalize.

So lets take our full-on string type and well make another test.

Give it that.

Use an array again to test a couple of different cases.

In fact, we just wanna do really this again.

Now this is a type error.

Now these type errors are gonna be weird because we have a missing case here.

So never, actually theyre both never. (laughs) Ive never seen a never come up when using this in actual code but, of course, right now, its halfway implemented so its gonna get confused.

The final case we need.

We can guess at what it is by looking at these two closely.

Both of these start with colons, but not all patterns start with colons.

In fact, this pattern that were working with doesnt start with a colon.

So we need something where if theres anything else at the front, we wanna just ignore.

We wanna just chop that off so we can see another string that starts with a colon, and then these two can take over.

So lets actually do that.

Its gonna be the final case.

Were gonna say no colon there.

Were gonna have some stuff at the beginning that we dont actually care about.

This is something like /courses/ at the beginning.

We just wanna get rid of that.

So Ill put a underscore on it to sort of indicate that its thrown away, and then were gonna have a slash and then we will have our actual param, but I called this Rest, not param because there could actually be lots of stuff here, like this corresponds, oops, wheres my cursor? There we go.

This corresponds to this entire sub-region of our pattern.

So yeah, theres a param name here, but theres a bunch of other stuff after it.

So we have a Rest here and we need to recourse on it.

So lets recourse on not just Rest, theres one more thing.

We have to reinsert the, what did I do wrong here? I didnt close my literal string, okay.

We have to re-add the colon because if you look at this, how we disassembled the string right here, we sort of took the colon out and only captured what was after the colon.

So we need to put the colon back in because thats how these two rules are gonna know that its a param.

Incidentally, the ordering here is it has to be in exactly this order.

In fact, if we do this, were gonna get type errors as you can see.

The reason that it has to be in exactly this order is if theres a slash anywhere in there, then we really need to disassemble it to find out whether there are more params.

And if we put this case first, then its gonna swallow that slash up, and everything is gonna get really confused.

Now, this is (laughs).

Ive made this little anticlimactic.

You mightve noticed that the type errors here went away thats because this code is actually correct, huh.

So these two strings are allowed, but if we do a lessonIda, no good.

lessonIda is not assignable to courseId or lessonId.

So it is successfully handling all the other junk that we have in this pattern and finding only the holes, only the params.

Okay, so we have one thing left.

We can get the names out as a union, but what we really wanted was an object type.

So lets write a type.

Ill get rid of these tests.

We dont need those anymore.

Ill write a type params that takes pattern extends string, and we just need to turn that union of literal string types into an actual object type.

So Im gonna say, this is a map object type.

For each of the ParamNames of pattern.

Thats not right.

Here we go.

For each of those ParamNames, its a string.

We only take strings.

This does mean with this path library if you were to, you cant pass a number and you have to string it, for example.

I dont know whether its technically impossible to actually put type information in here somehow, but I dont wanna implement that and Id rather just convert everything to strings.

There, youve got to establish some boundaries about how large your types are gonna get.

This thing is already real big and pretty scary.

Anyway, again, kind of anticlimactic, but it started type checking.

Now lets make our lesson path again.

Lets actually copy it verbatim from the real thing.

So we have our lesson path and now we should be able to call it with courseId of, we dont actually care, lets say typescript.

And lessonId of, what was the lesson in the browser? Oh, you cant see it there.

I think its recursive types.

I think thats the name that we were using.

That type checks.

Now, if I lessonIda, no good.

Lets go over here to see the full type error.

Object literal may only specify known properties, but lessonIda does not exist in that type.

If we omit one of the parameters, did I write that? Yeah, I did.

Then we get lessonIda is missing in type.

And finally, if we provide even an extra one, thats also not allowed because object literal may only specify known properties.

Now you could play games theoretically get around that.

But again, this is designed to be used in this way.

Youre just directly passing those patterns in.

Now, from this point, there are a lot of places we could go.

Obviously, we could actually implement the body of the function.

I mean, you can probably guess what this looks like.

Its gonna be something like youre gonna split that pattern on the slashes.

And then youre gonna map over them and do something.

And then at the end, youre gonna join them together with a slash again.

And you know, its just JavaScript.

The interesting thing here is the TypeScript stuff, at least in my opinion.

Now this is used in production in execute program.

Weve been using this in production for six months or so.

Before that for about two years, we had a much clunkier version because this particular language feature, oops, I cannot use vim today.

There we go.

This particular language feature, the template literal type inference was not there until relatively recently.

As soon as I saw this announced, I was like, I know what this enables me to do.

Lets me clean up my horrible path management library and make it very nice and clean like this. (chuckles) So it is in production in a real commercial system.

It is also on NPM.

The few seconds ago here will not be true when you see it, but it is static-path, which I think is a very fitting name under my GitHub account right there.

And finally, if you are interested in execute program, we do have this pretty comprehensive TypeScript course.

If I scroll down here are all the topics in there is quite a lot of them.

By the time you see this, therell probably be somewhere between two and 2.5 times as many topics, cause I have to record this a month in advance.

Were close to a release of a much larger set of TypeScript content, including every single type system feature we saw in this talk.

Conditional types, all that advanced stuff.

I talked about everything, I think I did.

We built the path thing from scratch.

I showed you that its open source static-path.

Talked about execute program.

Hopefully, some of this is useful to you either as a nice example of sort of what all these advanced TypeScript types are actually useful for, because its easy to find examples of, you know.

It makes sure that your string doesnt get used as a number or whatever, but these kinds of things tend to usually be very large and complex.

And this is a nice example because it has all these advanced features, but its only two types, ultimately.

So thank you very much for watching and enjoy the rest of TS Conf.