Video details

Angular Ivy Typer-Checking | Alex Rickabaugh


Alex Rickabaugh

Angular Ivy features strictTemplates, a compiler flag that turns on much stronger validation and type-checking of component templates. Learn how the new type-checking works and how to migrate a large project onto stricter type-checking with Alex Rickabaugh from ng-conf: Hardwired.


All right, hello, everyone. Hello and welcome to stronger type checking and templates with IVI. For those of you who don't know me, although you should, because Aaron just introduced me. My name is Alex Workable. I'm one of the engineers on the Angular Framework team. I've been on the Angular project for over five years now. I joined before the two point zero alpha releases and for the past couple of years I've been working on the angular compiler and hoping to get it ready for angular. I've. And today, I'm excited to tell you about my favorite new feature of the compiler that we've been working on, and it's my favorite feature because we've been getting issues for literally years from users asking for stronger and stricter type checking and templates. And we've always kind of had to explain that architecturally. This just really was too hard to do with the way the previous compiler worked. So it was a real joy is part of building the new compiler to develop a new type checking mode that we call strict templates. So you probably already know that the angular compiler performs type checking of your application templates and maybe you're already using the full template type check flag, which enables the strictest possible checking that we had in Engler's version eight and below. So strict templates turns on an even stricter mode of checking with Ivy, and you can think of it as being similar to type scripts own strict flag. If you're not using this flag, I highly suggest that you do. If you turn on strict, true and typescript will give you the strictest possible type checking that it can do and it definitely will help you catch more bugs and build time instead of runtime. So strict templates is basically our strict true for templates. When you turn it on, the angular compiler will infer much more accurate type information inside your templates, and that leads to more accurate error messages and more bugs to build time. So yesterday, you probably saw Brian love, Brian loves talk on y type checking strictness is so important, it's really effective to be able to catch errors early in the development process when it's easy to fix them as opposed to catching them in Cuba or even worse, having glitches or runtime crashes in production. But another thing that I've noticed in the applications that I write myself is that if I'm using the type checker to catch kind of the basic, more obvious types of errors, I tend to focus my test suite on catching actual logic bugs and kind of more subtle things. So not only does catching errors earlier improve reliability, but also leads to a much more effective test suite that kind of catches a complementary set of problems. And so I'm going to walk us through a number of examples showing what the new templates can do and to make things easier, I base them all on this common user interface. So a user is an object that has a name property. That name has first name and last name fields, both of which are strings. And because of using one interface throughout, you can focus on the examples of not trying to figure out the different types involved. Another thing that I'm going to do is highlight all kinds of expressions within the examples that I show you, and this will show what is or isn't checked by the compiler. So anything highlighted in green is an expression for which we have what I would consider to be perfect type information. We know the most about it and we can check it more or less completely. Anything highlighted in orange or amber means that while we are still checking it, parts of that expression we have incomplete information about maybe our compiler is inferring in any somewhere in any tend to be poisonous and pollute the expressions that they're involved in and turn them into themselves. And of course, anything in red is something we're not checking at all. So first up, let's talk about how the new type checking mode differs from the previous two, then we'll talk about how to go about turning it on in your applications without kind of getting overwhelmed by a stream of template errors. And finally, we'll talk about how this feature works under the hood. So to start off, I think it's useful to talk about the two modes of checking available and angular eight and how they're different. So the first is the basic mode of checking that every application gets, even if it doesn't set any flags, and then there's the full template type check mode, which enables stricter cheffing. That's been the default for some time in the Seelie. So here's a pretty basic template that's going to show us nicely the main difference. We're going to display a user's first name, user name first. And if the NGF is true, we'll also show their last name, user name, DOT last. And even in basic mode, with no flag set, Angular Version eight will still check a few things about this template. It's going to verify that user name first is a valid expression, and that includes checking that user is actually a valid property of the component. The Beretti will also check that show last name is a valid property and that NGF exists and is something that we can find to. But it's going to miss a few things, it's not going to check that the type of show last name is actually assignable to the input of NGF, thankfully it will be because NGF checks truthiness. So it will accept anything and it's going to ignore the contents of any embedded views. That's any engy templates. Any structural directives like Injia for Engy for anything inside of those won't be checked at all. So it won't check the user name. That last expression. And so if we accidentally write, use their last name instead, the template type checker is not going to catch this. It's going to be a UI glitch at runtime. We'll probably show a blank last name. Infl template type check mode, things get a little bit better. Angular is now going to correctly check the embedded views, the contents inside the Angoff, so it's going to validate the user name that last is OK, but it's still not going to check that show. Last name is actually assignable to NGF. And that's because the full and full template type check actually means it checks the entire template, both the top level template in any embedded views like NGOs or engy force. That's the main difference between these Moat's. So now let's talk about IV and strict templates and run through a few examples. I'm going to show you four of them where our type checking has improved significantly. So first, I want to talk about type checking of engie for loops also that check a binding excitability. So making sure the inputs of directive's are components are actually compatible with the expressions that you bind to them. Inference of the event Type four output findings and last but not least, some fixes to strange behavior around the type checking of safe navigation. And since full template type check is actually the default in the Seelie, that will be the basis for a comparison. The best type checking annually could do inversion eight versus the best we can do in Ivy. So the first example I want to show you is an energy for loop that iterates over an array of users and the ability to check energy for has been one of those most requested features. And we can finally do it with. So what would happen in Virgini? Well, even in full template type check mode, the compiler couldn't understand very much about an engine for again, it'll validate that the directive exists, that we can buy into it. And the theory we're passing in is a valid expression. But it's not actually going to check, for example, that our users expression is actually an enterable and therefore assignable to an entity for. And withinside the energy for body, the compiler actually has no way of knowing what type the loop variable is, so it treats it as any. And that means that our expression's user name, the first user name, that last or actually all kind of fall apart and turn into Enys. And so if there were typos in there, they would not be caught by the type checker. Here's kind of a similar form of that er the compiler is able to verify the get last name as a function is something that exists on our component and that we can call it. But because the loop variable is inferred is any the compiler can't provide any safety around using it as an argument. Forget last name. So under strict templates, this picture gets a lot greener. Thanks to our new template type inference, the compiler will actually be able to tell what type the loop variable is so we can correctly type and check all of the expressions involving it. This is the first time, as I mentioned and angular, that we can do this. So one of the main differences between the previous type checking and engie and strict templates is to check for assign ability of inputs. So in this template, we're invoking a component called user component, and we're passing in an expression, my user. So previously, the type checker would verify that the expression was correct and that the input user exists, but it wouldn't actually check that the expression was compatible with the input. And so if you did something like pass in a string instead of the interface type that it expects, this would not be caught by the type checker and this would probably crash at runtime. So one case where this can lead to some really subtle bugs or crashes is when you use the async pipe to wait on unobservable, for example, if you're fetching data from HDP. And until that observable actually emits data and expression involving the async pipe will actually become null. And that means that if you find an input to it, that input is actually going to get set to null until the observable emits some data. But because that wasn't being typed checks, this could lead to other crashes or glitches if the input wasn't actually expecting a null value. So with strict templates, we can actually tell you about this at build time. The compiler knows the async pipe is going to produce either a user or null, and it can check that both of those values would be compatible with the user input of our component. And if they're not, it can with an error message. A similar thing happens for output bindings and the dollar events value dollar event has previously been inferred as the compiler will verify that the event that we're binding to exists. But it won't be able to tell if what we're doing with dollar event actually makes sense, given the output type of the event binding. So with strict templates, we can figure this out, the compiler will correctly infer the type of event, it can check every part of the binding as a result. And the last example I want to show you is safe navigation. So in this case, let's assume we have a component here that has a user input that may or may not be set, so it might be undefined. And we'll declare a function in that component called format, name and format name is going to take the name and as a string and presumably format it for display. I'm just calling String that Treme as an example. So naively, we may attempt to print out the name and our template using format name. However, even in version eight, the compiler will correctly complain about this, it says the user property can be undefined and therefore you can't just access the name property of it. And that's correct, but we can work around that ANGULAR has safe property navigation. Let's use it. That will allow us to do user Westendorp name and not crash if user is undefined. Unfortunately, there's still problems with this because even though it won't crash if users undefined the result of the overall expression, if a user is undefined, is actually undefined still, it still has a value. And that's not valid to call for that name with. But the type checker here hasn't helped us. It doesn't know this because it turns out some safe navigation operations and angular or inferred is any. This is actually a bug in the old compiler. It wasn't trying to do that, but it worked out in certain cases that this happened. So under strict templates, as you might guess, we fix this, we actually catch this there. Now the compiler knows the overall expression is capable of being normal and can tell us that's not legal to call format name with. So the takeaway from all of these examples is that strict templates gives you much better type inference within your templates and that translates to fewer runtime crashes or other glitches when the assumptions that you're making about the data you're working with aren't quite correct or don't line up with what the type system says. How do you get there from here, though, if you have an application, I think someone asked this question on the screen yesterday. If you have an application that has like hundreds of components, it's not trivial just to turn on a new type checking flag and subject all of your templates to much more rigorous validation. There's probably going to be some failures, and it's annoying to have to fix all of them before you can turn the flight on. Unfortunately, we've designed strict templates for incremental adoption, so you can do that in four steps and step one is the easiest, you turn on the flag and drink from the fire hose of failures. Then you look at each of the failures and try to figure out why is it failing? Is it failing? Because we're now checking that an expression is assignable to an input and it's not. Is it failing because you're relying on safe navigation, inferring in any and once you have that list of root causes, which is much shorter, hopefully, than the list of errors, you can selectively disable specific checks to hide them until you get them all fixed. And that means that you can turn on this feature incrementally, one check at a time by disabling everything that's problematic until you've had time to go through and clean up your code. So there are a few reasons that you might have to do this. The most obvious one is if they're actual type bugs in your templates. But another thing that often comes up is if you're using libraries that weren't actually designed to be strict templates, compatible one cases, if they weren't compiled with typescript specific strictest settings, they might not specify null or undefined as valid values for an input, even if they're perfectly happy to accept that at runtime. So if the check that expressions are actually assignable to the inputs that you're binding them to is problematic, you can turn that off by setting the strict input types flag to false. As again, as I said, you might do this if libraries that you're using don't have quite correct types of their inputs or if you've been kind of loose about that in your own components. A special case of that is handling expressions which are potentially undefined, and that's because if a library is not compiled with typescript strict checks, the files that it produces actually don't include undefined as possible values. And so by turning off Strix, no input types, you can bypass that particular issue and still have the input excitability check working overall. Another problem tends to come up with attribute style bindings as opposed to the square bracket prop. bindings, attribute bindings are always strings and that can cause issues if you're trying to bind them to inputs that might be expecting, for example, a boolean value like a disabled input. And you can set strict attribute types of false to work around that. And so you have time to kind of implement the changes in templates. If you happen to be relying on the previous inference of any types for safe property navigation, you can restore that behavior by setting strict safe navigation types. And the same goes, for instance, of the type four outputs and output event types. And in the case of DOM events, we have another flag event types that affects the inference of events for those one case we've seen where people need to turn this off is if they're making extensive use of events that target, you might assume, as I did when I first saw this example, that if we're subscribing to a change events of an input event, that target will be an input element. But Dom, events can bubble and so does the event that target might not actually be the elements that you subscribe to in the type system is actually much more generic and that can break patterns like this. There are a few other flags that I haven't specifically mentioned, they tend to come up a little more rarely in application code. You can read the full list along with some descriptions of why you might want to turn them off on the template type checking guide on ANGULAR. So now that we've talked about strict templates and how to use it, I want to give you a small peek into how it works internally, how this feature was built more because I feel like it's really cool. So let's go back to the energy, for example, from earlier. I'm only going to talk about it superficially and not go into great detail about the type operations that are happening. I'm working on a blog post that's going to have a lot more kind of a technical approach to it. So here's our example template, we're iterating over a list of users and we're printing out the first name of each one. And right away, we run into a problem because TypeScript doesn't actually understand each HTML or angular templates, so if we want it to check these expressions, we actually have to convert them into typescript code. This is code that you as a user will never see. This is code that does not get translated into JavaScript. It does not run in the browser. It does not run anywhere. Actually, we feed it to TypeScript and allow TypeScript to check it for us and give us back any error messages. Then we take those errors and show them to you in the context of your original template. So that simple energy for produces this complicated looking piece of tape. And it is complicated in places, but what it's doing overall is fairly straightforward to understand. So let me walk you through it. The first thing it's doing is declaring a function called a type constructor for the energy for directive's, and that's because the energy for DirecTV is generic. It's type actually depends on the types of the inputs that you pass in. And so what this type constructor function does is given a particular set of input bindings for energy for figuring out what type the directive is going to be. The next thing this code is going to do is declare a function called a type check block, and this represents the entire template in zip code. So it has one argument, which is a context variable that represents the application type. And the first thing this block does is call the type constructor for energy for. You can see it's passing in the binding expression that we wrote for Andy for just context, users will let us know if that's not valid here. And then we get back this value one, which is actually has the correct type for the energy for DirecTV in this particular instance. It'll be an engine for of user objects. Next, we ask the engine for structural DirecTV, what type it's going to use for its template context at runtime, engineers free to create embedded views of any type that it wants. So we have to ask it, hey, what are you planning on doing? And then we'll tell us this too variable should actually be narrowed to a type called Engy for context. That's generic over user objects. And inside this block, we can now extract dollar implicit, which is the internal name and angular for the user variable. It's actually short for user equals dollar implicit, and that's going to be the actual loop variable of type user. And that's called t three here, then all that we have to do is reflect the actual expression T three name, name first. And TypeScript, tell us if there are any problems with that. Also, you might notice in the in the example here, there are comments with pairs of numbers kind of littered throughout the expression, these are our source maps. So if we get an air back from TypeScript, we can look at where TypeScript says the air is inside this code, find the nearest comment and then use that to map the air back to the original template so that when we show it to you, we can show it to you in the context of the binding expression that you wrote and not the code that we generated to check it. So in summary, you should use templates. You should turn on Strickland and TypeScript as well, if at all possible, and don't be afraid to disable specific checks while you're migrating. It's much better to be partially stripped for a while as you fix up errors in your templates than it is to never engage the flag because you're just don't have time to go and fix all the errors in one sitting. He has a very, very powerful template type inference and type checking engine, and we have a lot more ideas about new checks and new things we can add. For example, I don't think most bindings are checked at the moment. And with our strict this flag model, we can actually turn these on incrementally over time and roll them out to you without major inconvenience if you've already enabled the flag. Because if a new check is too Brachy, we can actually release it as an opt in vote. So that's all I have for you today. Thank you very much.