Exploring Static Types: Writing Typesafe Code that Feels Like Real JavaScript.

- All right, thank you very much.

Okay, like Loughlin said today, I'm going to be talking about static type systems in JavaScript.

So I'm going to be looking at whether we actually need static types in JavaScript, how static type systems work and also how you can kind of use these systems without cluttering up your code base with a bunch of annotations that feel like they're just getting in the way and they're there for the type checker and not so much for you.

So, like Loughlin said, I work at the ABC right now. I've worked at Facebook in the past.

And I use JavaScript and React in my day job. And for the last couple of years, I've been using Flow to help me write JavaScript.

So I want to start by addressing this problem. And moving my mouse away from that window because that will distract everyone.

So, does JavaScript need to be strongly typed? And the answer is not an obvious yes.

There's a lot of use cases for code that is dynamically typed and is a little bit more loosely typed.

And not every language does need to have a strong typing features.

And JavaScript has survived and almost thrived for a long time without them.

But really JavaScript was invented for a different use case from what a lot of us are using it for today. So its invented for really the one-liner where we need some interaction to happen on the page in response to some kind of dumb event.

So it has things like automatic semicolon insertion and type coercion.

And you can even write a lot of JavaScript without using functions or classes at all.

And now that we're writing things like web apps and we're moving critical functionality into JavaScript, we need to have higher confidence that the code that we're writing is not going to break when we ship it out to production. And also we have things like unit tests that help us do that.

And of course we all should be using unit tests. But there are a lot of other tools and conventions that we use that are actually already based around types. So we might have a guard at the top of a function that will throw if we pass in the wrong type of value. We might have comments in the form of dot blocks that inform other programmers about the expected prototypes and what we're actually returning from that function. We might use Hungarian notation which you probably think doesn't exist in JavaScript but if you've written in Jquery, you're probably familiar with this convention of prefixing variable names with a $.

If they have hold a Jquery object.

And we can also use object-oriented code in the classical way to just compose object hierarchies and class hierarchies that we can use instance of checks on. And all these are examples of us enforcing the correctness of our code.

And we're doing that using the tools that we have available to us with plain old JavaScript. And all these conventions are all about expressing and clarifying the intent of the code and the intent of the offer.

And they do that in a way that merely just writing the code so that it works, is not going to do.

And we're going to show you an example of what that actually means.

Let's say I'm looking for my code base one day and I come across this example.

And I go, what is this doing, why does this exist in our code base? I have to say, well, it's probably just taking an array of numbers and going to return the sum of it.

But this function does a lot more than that. So I can actually pass in an array of strings and it's going to concatenate them together for me. I could pass in a string and it's just going to reflect that back at me too. And I could also pass in some fancier data structures, things like generators or maps.

And that's going to happily consume those.

And from looking at this code, I can't detail the intent of the offer in the first place and this adds a huge overhead to working with code that you might not be familiar with. If I wanted to go in and refactor this code, I have to consider that maybe somebody is actually relying on one of these edge cases and in a non-obvious way too.

And so that's why we use the types of conventions that I mentioned before, things like dot blocks.

So there's a big difference between code that solves a single problem and that makes sense to an engineer on the day that they're actually writing that code, and code that's understandable and predictable over a long period of time.

And so, one of the takeaways that I want you to have today, is this idea that a type system is, first and foremost, about communication.

And static type systems continue this philosophy. And I'm going to show you two such type systems today. If you're using types in JavaScript, there's a good chance that you're using either Flow or TypeScript. So TypeScript comes from Microsoft.

It's created by Anders Hejlsberg, who's the creator of Delphi and C#.

And Flow is from Facebook, and they use it in their web and their native stacks and it happens to be the system that I'm most familiar with. So they both introduce a syntax for type annotations to your code.

And this example would actually work in either of those systems.

And most of the examples that I'll show you today will cut across both systems.

Let's say that I'm building a pizza delivery service using robots and I want to make sure that my robots are gonna actually get to where they should be going.

So I'm gonna use a type checker to increase my confidence in my code.

So I can add some annotations and say that something is a string or a number or an array of strings.

But that can get a little bit wordy so we can use something called type inference to cut away a lot of that crush that we're adding in. So here we can see that Flow is saying that 10 times 5 is always going to produce 50 and is always going to be a number.

So it's going to give me the same guarantees as if I actually said that the variable total is holding a number. We also get some great tooling in integration too. So this an IDE or a complete that you'll get. This is actually VS code.

And it will know the type of the variable.

So it will actually tell you what methods or what properties you can reference on that variable. You'll also get some information when you hover over a variable in the IDE, it will tell you what type it is.

We have some command line tools too.

This looks a little bit messy but it's a nice way to run the type checker without having to integrate with an IDE.

And this will obviously integrate your build pathlines and CR pathlines and all that kind of stuff. There are a couple of differences between TypeScript and Flow, though.

So firstly, TypeScript is what's called a strict syntactic superset of JavaScript.

It sounds really cool, but what it basically means is that it's not truly JavaScript, it adds some language features on top.

So you introduce things like enums and mixins and private and public fields.

So it has a compiler that will take TypeScript and produce available JavaScript that you can run in the browser or run in Node. Flow is really just JavaScript under the hood. It just adds annotations on top of that.

You do still need to compile it to strip out those annotations so that you're left with valid JavaScript syntax.

And one of the other differences is a little bit more subtle.

And this is one of philosophy.

And this is how both of the systems think about correctness. So in type systems you have a concept called soundness. And a sound type checker is never going to let a programme with a type error execute.

So it's going to stop you.

And Flow has the goal of being sound.

Whereas TypeScript is actually okay letting some errors through if they think that's a good developer-experience trade-off. And I'll come back to this a little bit later because there's some implications of those choices. For the most part, Flow and TypeScript are very similar in terms of philosophy and functionality and also syntax, so don't sweat the choices if you're on the fence between these two.

You're probably better off just picking one rather than deliberating too much.

And of course, the big selling point for both of these systems is the fact that they can find bugs in your code.

And they do this with something called static type checking. So you might be familiar with dynamic type checks like you'd find in PHP, or even languages like Lisp, where you don't know that you have an error until you're passing in a value and you're code will either throw an exception or it's going to continue on as it should.

And with those systems really relying on having good unit test coverage to make sure that you're testing all of the edge cases in your code base.

And even if you do have good unit test coverage, you've got to stop coding and start running tests before you get that feedback about a problem. With static types, every possible error is surfaced all at once.

And you're going to get that feedback immediately. So with Flow, as soon as you save your file, it's going to tell you if it has a problem. And with TypeScript, you get that feedback even sooner. It's as you're actually typing the code, it'll mull out the errors.

And finding every possible error all at once, it sounds kind of magical, but conceptually what's happening under the hood is fairly simple.

And I'm going to show you how Flow approaches this problem. So here I've got some code and I'm calculating the price of what is in my system.

And I have a bug because I'm passing the name of a pizza as a string into this function instead of a number. And I'm trying to do some arithmetic with it. And this system is gonna start by looking at this code and it's gonna record the annotations that I've put here.

So it's gonna say, I see that we have a string type and it's flowing into this type used by the pizza variable. This is how Flow records all of its constraints and all of its observations.

We always have a type definition flowing into an operation. So it's going to augment this with some observations that it makes on its own.

So we can infer that these variables are going to use number types.

It also understands things like function calls and if segments and other JavaScript constructs. And it can record about usage of arithmetic and that kind of stuff.

So the system has now built up a huge set of constraints about how we're using types and it can start to try to join these facts up. So we can see that we have a string flowing into the PizzaType and then the PizzaType is flowing into the PriceArg.

So we're forming these chains of constraints. And then we can start collapsing them down. So we're just left with the very first and very last parts of each of these chains. And we can see that we're using string in an arithmetic operation which is no good. And so this is actually showing, in our IDE, where the error actually occurred because the type system, when it's recorded, each of these constraints and observations, it's recorded the line and offset of where that code. And I think, at least for me, understanding what's happening under the hood is really interesting.

But I think it's also really important for anyone who wants to use one of these systems.

Because any tool that you introduce into your workflow is going to have positives and negatives.

And I've worked with people and seen people using type systems.

And what they do is they just code the way that they did before.

And at the type checker is going to yell at them and tell them that they did something wrong. And they'll move code around a little bit.

They'll add some annotations.

But generally they're just gonna have a bad time and they're not going to enjoy using the type system. And what they're doing is they're seeing all of the negative sides of this tool without seeing or even trying to seek out the positive sides of using that tool. So when you see something like a type system as being magical, what you're doing is you're hoping that it's going to find bugs for you.

And you're hoping that it's not going to complain about your code.

But when you understand how it works, you can be an active participant in making that happen. And working with a type system in this way is related to the concept of soundness that I mentioned before. And I want to come back to this because I explained how a sound type system is going to reject all type errors.

In an ideal world, that would really just be the end of the story. But in practise, a type checker is never going to be able to understand every single piece of code that you write for JavaScript.

JavaScript was not created with static type checking in mind.

And if you do want a list of things that you shouldn't do to trip up your type checker, you can look at Midi's talk a little bit earlier. All that dynamic programming and reflection, this is stuff that the static type system is never going to be able to comprehend.

And if it can't comprehend code and if it says that it's going to reject code that is invalid, by definition it has to be conservative and reject that code.

And so there's actually a second dimension to our type systems here.

And that's how complete the type checker's understanding of your code base is.

And if we just look at that vertical axis, what happens when you're writing code that the type system can't understand is that you're at the bottom here.

You're in this pit of failure, where you're writing code that it can't understand. It's gonna give you a bunch of false negatives. If you're writing code with a type system in mind, you're gonna have a much higher signal-to-noise ratio. And I want to dive into some code now and look at patterns for how we can actually do that. We're gonna start with the basics with primitive types. So with a type system you can say something is a string or a number or a Boolean, which is really useful but also hopefully not surprising that you can do that. What might actually be surprising is that you can do something like this.

And you can use a specific literal value.

So I can say, you can only ever pass the number five into my function.

That's not super-useful on its own but you can combine it with something else. And this is called a union.

So a union is where you say a type is one of multiple things.

So my star rating has to be 1, 2, 3, 4, or 5. And if you ever try and pass another number or another string into one of these functions, you're going to have a type error.

Unfortunately, not every type can be listed out ahead of time and sometimes we just don't even know if a type is valid until runtime.

But thankfully we have some affordances for working with that in Flow.

So let's say that I want to be able to email people when their pizza is ready, so I'll have a send email function.

And this accepts an email address in the form of a string. Now I might be using the email address in a lot of different places.

And I might be tempted to try and validate that every single time I encounter it and do a REGEX check and throw if it's not what I expect but that feels a lot like dynamic type checking. Instead we can use something called opaque type. So I'm creating a type called email and it's just an alias for a string.

So email is a string.

And then I can use that inside this function called createEmail.

So I'm passing a string, if it's not valid, I'll throw.

If it is valid, I'll return it back but I'll actually say, well, it is now this special type of critical email. And I can use that type and that function in other parts of my code base.

But it's called an opaque type because even though other modules can know that this code exists and this type exists, they're not allowed to know that under the hood, it's just a string.

So if we ever try to do something like this where we're assigning a string into the email type, it's gonna throw.

And so a nice guarantee that I get from this is that every single time I see an email type anywhere in my entire application, I can know it has gone through my create email function. It's done the validation and it's the exact type that I'm expecting it to be. All right, I'd like to look at some more complex type sets. So let's look at objects.

So Flow and TypeScript, they have two different ways of working with objects.

We have nominal typing and structural typing. So nominal typing is based around the names of types. It's probably what you're familiar with if you've used C# or Java or something like that where you say, is something an instance of some class. Structural typing is a little bit different. I think languages like Go were using this too and I think maybe Rust.

And this is where you describe what you want an object to look like, what properties it should have.

You can create an object anywhere and as long as it's got the correct properties, then it's going to match that type.

And this feels really home in JavaScript because we are used to write a code like this where we have a bag of values, we kind of just want to scoop them up into an object and pass them around as little record types or structures.

And you might have noticed this little question mark on the address property here and this is called a optional property.

We need this because by default, type systems like Flow and TypeScript, they assume that everything has a value and isn't null or undefined by default.

And they do that for a very good reason and it's to stop these terrible exceptions. These are the bane of every JavaScript engineer ever where we're trying to call a function on something that doesn't exist or we're trying to access a property on a null object. And these are always the worst because what you'll do is you'll go into your code base and you'll have to find that one weird edge case where you're not doing a null check or you're not assigning a property you should be doing. And you'll probably actually just end up putting up a pull request that looks like this. Has anyone ever written this kind of code? Yeah, this is pretty idiomatic, it's terrible. Thankfully, we have the stuff that we were seeing earlier from Kyle that will make this a little bit nicer to do. But with types this is not so much of a problem anymore. And this problem exists in JavaScript because of something called null references. So in the very first programming languages, if you said that you had an integer, you have to actually provide an integer value. You couldn't assign null to it.

So someone actually invented the idea of saying, hey, it's okay to put null there as well.

And it was this guy called Tony Hoare.

He did it back in 1966, a long, long time ago, over 50 years ago.

And many, many years later, he looked back at this and was like, what did I do? Why did I do this? And he called this his billion dollar mistake. Which I think is a pretty amazing claim to fame. And for me, getting rid of null references is a no-brainer. It makes your code a lot nicer if you say, hey, I want a string and then you can just start working with that string. You don't have to worry about doing that ceremony of doing null reference checks or doing early returns or throwing if it's not exactly what you expected.

And if you are willing to accept a null or undefined value, you can use a nullable type.

And a nullable string is actually just an alias for another type of union.

So it's a string or a null or undefined.

And you'll notice that a trend is that unions are super-powerful in type systems. And I'm gonna share one more code pattern with you. And this again uses unions but it also shows some examples of nullable types and structural typing.

So let's say in my app, I want people to be able to give me money which is really important when you're building any kind of application.

So I need to track payment successes and failures and all of that kind of stuff.

So I've created a type here that represents what could be happening in my code base.

And it has some optional and nullable types because error code and success message aren't applicable to all of these different states. Now if I was to go and use this in my code, Flow is actually gonna pull me up on this because I've said that error code could be undefined. I can't just go and access it.

I have to do a null check on it first.

As programmers, we know that this is wrong because we know that if we have a failed status, that we're going to have an error code.

But Flow has no way of understanding this.

And so we can fix this with something called disjoint unions.

They have a few other names and they're all equally as scary.

But essentially what it means is that a union is where we have one of multiple things. So the type can be many things.

And we have some way of differentiating at run time exactly which of those things it is. So I'm gonna redefine my type and I'm gonna say, instead of being an object with some nullable properties, it's going to be one of these object types. It's either an object with status of pending, an object with status of failed, and an error code, or an object with status of completed and a success message. And already from a documentation perspective, this is much more readable because we can see exactly when we need to provide each of the properties. And Flow and TypeScript can make the same kind of leap that we did.

And if we could go and use this in a code, Flow can actually understand that inside this if statement, because we've done that check against status being equal to failed, we know for certain that we have just this one object type and this is called type refinement.

And if you tried to write this type of code before, like this kind of state machine code, you know it's really hard to track which properties should and should not be set at each point in time.

But if you design that same code around disjoint unions, then all of a sudden you're given more guarantees and more confidence that your code is doing the correct thing.

And that's a really powerful concept because now type is not just reflecting what JavaScript that you have, but they're also informing what JavaScript you should write in the first place. And this taking us into the realm of something called type driven design, where you think about and design your algorithms around types, you know, what are we starting with and what are we hoping to end up with.

So types are the key frames in your application. They define the pivotal moments.

And you can get a sense of what's happening without looking at the mechanics of how to make that happen in the first place. If I say that I have a blue star, and it turns into a pink pentagon, that kind of tells you everything that you need to know. You don't need to know the low-level details. If you had started with the details, started by running JavaScript, you might be tempted to handle the edge case where someone gives you a square or a circle. What if someone gives me an object and it has no colour, better define a default colour.

With type-driven design, you're thinking about the interfaces first. And you're motivated to make them as simple and as clean as possible because that's the level of granularity that you're working at.

And it turns out that most of the time, that's actually just all you need.

You don't need all of those other edge cases that you like to code for.

So you just naturally end up with simple and clean code too.

All right, so that's a look at static type systems in JavaScript.

We've covered a lot of ground and we've looked at how we already make use of types in JavaScript to enforce correctness, how type systems are about communication, and expressing intent, and that you need to understand your type system to really try and get value out of it.

And I showed you some examples of how we could do that. I think that Flow and TypeScript are both really great projects and when used well and for the right reasons, you end up with code that's both robust, but, I think, even more importantly, that's simple code.

If you would like to know more, I'd encourage you to check out the official documentation. So both projects have really great documentation. Flow even has an online editor where you can get a sense for how everything feels in practise.

And I have shared these slides, some links, and some code examples on GitHub. So if you just go to that second link there, you'll just jump straight across to GitHub. Thank you very much.