 Hello, everyone. I'm Chris Crycho. I'm a senior staff software engineer at LinkedIn, working on the road to TypeScript for Ember, taking things from where we've been good to great. Today, we're going to talk a little bit about how we got here, a whole bunch about where here is, and then just a little bit at the end about where we're going from here. Now, since we're at EmberConf, you might expect that this is a talk for Ember developers, and of course it is. And for Ember TypeScript developers specifically, and of course it is. But it's also a talk for Ember JavaScript developers, and in fact it's also a talk for non-Ember TypeScript and JavaScript developers, because many of the things we've been working on are applicable not just to TypeScript, but also to JavaScript, and not just to Ember, but to the broader ecosystem. So let's jump in. How did we get here? There are three broad eras to Ember's history with TypeScript. The before times, the early days, and recent history. The before times run from 2011 through about 2015. Ember itself appeared in 2011, TypeScript appeared in 2012, and they ran in parallel without much overlap other than collaborating on the start of the decorator's proposal all the way back in 2014, until a couple of key things happened in 2015. Ember itself started using TypeScript and TypeScript put out support for JSX in the form of TSX, typed templates, which really for the first time gave front-end web developers the ability to have end-to-end types for their JavaScript code. Really powerful. And at the same time, Ember began investing in the internal implementation of its rendering library for its template layer, the Glimmer VM, in TypeScript. That brings us to the transitionary period, the early days. In 2016, near the end of the year, TypeScript put out a couple key features, key oven map types, if you're curious, which unblocked the ability to actually represent Ember's classic approach to building things in TypeScript. It was a little janky, but we could do it. Prior to that, we hadn't been able to. And so in 2017, a group of us got together and we made Ember CLI TypeScript work and we built out type definitions on definitely typed, the at-types packages you're all familiar with. And after that, we settled down into a SETI state over 2018 and 2019. Things were fine. They were actually decent. You could get things done. But there were some gaps. And so in the intervening years, since then, recent history, we've started trying to address them. One of those gaps was semantic versioning. We're going to spend a bunch of time on this. So for now, I'll just let it be said that starting in 2019, LinkedIn was really researching hard. How can we solve the semantic versioning and TypeScript problem? By which I mean, the TypeScript team doesn't believe Semver makes sense for compilers because literally every change you make breaks somebody. We don't actually agree with that, but it's a given in the ecosystem and we can roll with it. We spent a bunch of time over a few years, Mike North first and then me, actually getting a handle on how can we solve those problems? Then in 2020 and 2021, the good folks over at Salsify, Dan Freeman and James Davis, did some hard work to say that innovation that happened in TSX, what if we could do that in Ember? Can we get type-aware templates for Ember and glimmer users? And the answer is yes. The fruit of that research was Glint, a tool which uses TypeScript with Ember templates. And we'll see more about that later. And then finally, starting in early 2021 and running up through now, we've been working on official Ember TypeScript support. A lot of RFCs and that brings us to today. Like I said, a lot of RFCs. There are many things we've had to address to make this viable. If I just tried to work through all of these in order, all of your eyes would glaze over and you would tune out. Not least because the order that we wrote them in isn't necessarily the order that makes the most sense, it's just the order that happened to get done. So instead I'm going to take them by chunk. A couple on philosophy, one on the details of how Ember is going to apply that philosophy. A whole bunch on the changes to authoring code we're making with an eye to making things better for both TypeScript users and JavaScript users. And then finally, docs. And you'll notice there's a TBD there. There's a reason for that. Let's start with philosophy. RFC 724 said, we're doing this. We're going to make Ember have official TypeScript support with the goal that it's a positive sum outcome across the community and indeed beyond Ember. The how of that is some guardrails. Number one, we will never require TypeScript. You can think of this as the, we're not doing what Angular did, guardrail. And it was a fine choice for Angular to make but we're not doing that. We're making TypeScript appear to JavaScript, not something that replaces JavaScript, not ever. As part of that, we commit to doing the work during our design processes to make sure that all our investments in TypeScript actually actively improve the experience for JavaScript users in the Ember ecosystem. And we'll see a number of concrete examples of this later. We need to make sure we keep our stability guarantees, which means we need to figure out how semantic versioning works when dealing with TypeScript. We'll invest in specs like a sem-verse spec and tools, integration in CI, early warning about breaking changes, et cetera, to make this possible. And we'll ship types natively, not from definitely typed. Semantic versioning, we decided we needed to really think hard about. We took the insights that we had developed at LinkedIn over the preceding years and hammered them out into RFC 730. Among other things, this consisted of a very, very deep dive into TypeScript semantics because it turns out that figuring out what a breaking change is or isn't is hard when you have to deal with variants in type-level mutable variables. When I first started researching this, there was literally nothing on the internet about this at all. And if you go searching now, Ember RFC 730 is one of the top hits because this was original research we needed to do to solve this problem. And we think and hope what we came up with will be really useful across the TypeScript community. You're a node user writing a node library or a Dino user writing a Dino library or a Svelte or React library author. This can work for you. So what's the short version of it? Well, we've defined some policies for how you handle upgrading TypeScript versions, including that you need to tell people what TypeScript versions you support. And you need to test those to make sure that you don't break things accidentally for people. And then we came up with some rules about how to think about breaking changes. And this comes back to the old good advice about API design. Be generous in what you accept and conservative in what you provide. But we formalized it. Those support policies. As I said, everybody needs to specify what policy are you using and what TypeScript versions do you support and what constitutes your public API because those are the things that have to not break. And there are two broad options here. The simple majors policy and the rolling support window policy. Simple majors is simple. If you drop support for an existing TypeScript version that's a breaking change for your library. That's it. You can add support of course, but you can't make people upgrade their TypeScript version without it being a breaking change under this policy. And this matches ecosystem norms around things like node. When a node LTS goes out of support and we drop support for it, most libraries call that a breaking change. The other policy is a little more complicated. Things aren't necessarily breaking just because they drop a previously supported TypeScript version. Instead, long-term support releases whether you have that formally like Ember does or it's just an informal thing that you don't make breaking changes very often need to make sure that they have overlapping supported TypeScript versions because this allows people to ladder up. That is they can always upgrade to the latest supported TypeScript version on your library without having to upgrade your library and vice versa. Once they're up to the most recent version that your old library version supported, then they can upgrade to your new library version without also changing TypeScript. And this is key because it means that no one has to upgrade both at the same time because those are the painful kinds of upgrades. Nothing is as bad as trying to upgrade a library and having to upgrade five other dependencies at the same time and trying to figure out which one is causing all the breakage internally for you. That's terrible. This policy avoids that. And this matches the way foundation-level ecosystem packages again like Ember itself treat things like Node. When a Node long-term support release goes out of support Ember drops support for it. The key here is that semantic versioning is a socio-technical contract. And I could do a whole talk on this and hopefully someday I will but the key is that we keep our contract. So whether your contract is simple majors or rolling support windows, you just have to keep it and then people won't be broken by surprise. And in fact, if you do them as we've suggested in this RFC they won't be broken at all. Here's how this works in practice. Ember modifier is an ecosystem library in the Ember world using simple majors. It's currently at version 3.2 and if you look at its readme, this is what you'll see. It says we follow the current draft of the semantic versioning for TypeScript types RFC. We support TypeScript versions 4.2 through 4.6. We also test against nightly and we use a simple majors policy. Our public API is everything not in a private folder. Pretty simple. Things are a little more complicated for what we expect to be the case for Ember source. If we adopt the rolling support windows and spoilers for a later slide, we're doing that for Ember. Here's how it might work. Given that one Ember releases every six weeks and every fourth release so roughly twice a year is a long-term support release and TypeScript releases once a quarter. We can imagine that Ember starts publishing types using this policy at Ember 4.8. And at that point it supports TypeScript 4.6 and 4.7. Now when Ember 4.8 comes out TypeScript 4.8 will also be out. But we don't have to support a TypeScript version just because it exists. Maybe there's a big performance regression or a significant bug in TypeScript 4.8. We don't support it. That's fine. As long as we keep the rest of our contract. So we go along through Ember's normal release cycle and TypeScript's normal release cycle and when Ember 4.10 rolls around TypeScript 4.9 is out. And ah, this one works. It solved that bug in 4.8. So we add it to our support matrix. We also notice that it has a nice new feature we would like to use but not yet because we still need to support users back to 4.6. We get to TypeScript 5.0 release and the Ember 4.12 long-term support release. And hey, we have both of those now. We could drop support for TypeScript 4.6 here but there's no point. The new feature we want is in TypeScript 4.9 and we can't require that yet. So we don't, we just keep supporting what we have. We keep rolling through the normal release cycles by the time we get to Ember 4.14, TypeScript 5.1 is out. Things are going nicely. We get to Ember 4.16, another long-term support candidate and here, here's where things get interesting. Since Ember 4.12 supported up through TypeScript 5.0, we can go ahead and drop things earlier than 4.9 and now we get to start taking advantage of those fancy new features we want. The key is people could just upgrade while still on Ember 4.12 to TypeScript 4.9 or 5.0 and then they can separately upgrade to Ember 4.16 and go along their way, no big deal. Meanwhile, we've also added support for TypeScript 5.2 and the cycle continues. The key about this again, is that no one ever has to upgrade both of those dependencies at the same time. Only one at a time, it makes the upgrades easier. But the other key to this is that it lets Ember keep moving forward or other libraries doing the same thing. We're not stuck supporting TypeScript 4.6 only for the entirety of Ember 4's life cycle. If we'd done that, for example, in the Ember 3 life cycle, we'd even stuck on TypeScript 2.8. Even though by the time Ember 3 ended, we were on TypeScript 4.2. We don't want that. We want stability, but we don't want to get stuck. We don't want to stagnate. This lets us solve that. Going to the question of breaking changes again though, we said that the rule is basically be generous in what you accept and conservative in what you provide. And as a consumer, intuitively, you can think of this as the no new Red Squiggles rule. If I stick with TypeScript versions that the library supports and I follow the upgrade cadence that the library itself recommends, I won't get new Red Squiggles in my code from my library. Maybe I will from TypeScript itself. It might catch bugs in my code as new strictness settings appear. But fundamentally, the library should absorb those itself for its public API so that I don't get broken by them. Formally, well, as I alluded to earlier, the devil's in the details. I've shown here the very long, very detailed outline of December for TypeScript types RFC and it's long and it's very complicated. But it's also very interesting. If you're a library author, I recommend you take a look at it because this will help you think about the broad strokes but also the edge cases. You can find this at semver-ts.org. It's a draft. It's 1.0.0 beta one. We still want your feedback. We think this is solid. We've gotten good feedback on it already. But if you try it out and you find something, come let us know. We want this to be a resource to the entire TypeScript ecosystem. That takes us through our philosophy and don't worry, we'll move through the rest of this a bit faster but I think it's important to get the philosophy right if we're going to do the rest of it right. For example, when we get to the plan, it's a lot easier because our concrete semver commitments can be spelled out in terms of that policy. Core libraries in the Ember ecosystem, Ember source, CLI and data, we'll use the rolling windows support policy. Most other libraries will use simple majors just like they do with Node. Our breaking changes policy is don't do breaking changes except at a major like you would expect. There's an important nuance around additions that I want to call out though and that is that we say in Ember, we only have to support the current addition at a new major release. Ember uses additions as a way to gather up a bunch of incremental progress we've made and collect it all and make sure it's well documented and fits together. Things can get kind of messy in the middle but once we get to an addition point like we did with Octane and like we're planning to do with Polaris, it all makes sense again, altogether fitting nicely. In a major release, we can make breaking changes but in a minor release we can't and additions are always released in minor releases. This is a key part of how Ember handles versioning. So we can't say when we release an addition, our types only support that addition but what we can do is when we cut a new major, we can say we only support the current addition. So in the current world, for example, the types we start publishing from Ember are going to have minimal fallback handling for Ember classic idioms. We only commit to actively really supporting Ember octane features and we could do the same in the future. It's an escape hatch. It's one I don't expect we'll ever need other than the octane to classic transition but if we do, we have it and it's a useful tool. Again, semantic versioning is a contract. We just have to keep up our ends of it. We're also going to make a point to migrate everything from definitely typed to publishing from our own packages and this is really, really important for being able to uphold that stability story as well as to be able to do things Ember users are used to from Ember. For example, if we want to backport bug fixes, we can't do that on definitely typed but we can when we control the types and we can do that by writing type tests. Similarly, we're used to being able to support alpha and beta and stable as discrete things. We can do that if we control the types using ecosystem wide tooling that are not Ember specific. We can't do that in definitely typed. So this puts us in control of things that are really important for our stability story. We also are making communication updates. Our RFC template now includes how do we think about this from a TypeScript point of view? What do we have to teach that's different there? What are the actual types? Same thing goes for blog posts. If we add a feature or deprecate a feature in a way that only affects TypeScript users, we need to tell people. So that'll become part of our publishing process. And last but not least, docs, docs, docs but we'll get to that. That's the plan but to make it work we needed to make some changes to the authoring experience of writing TypeScript and JavaScript for Ember users. The first of these was first-class component templates, RFC 779. The goal here is to make component templates a first-class peer to JavaScript or TypeScript along with CSS. We can now use them side by side but with a different set of trade-offs than JSX and TSX has. The goal here is not to have HTML first or JavaScript first. They're peers in modern web applications as is CSS. So we want to keep the benefits of dedicated templating languages that we can use actual native HTML syntax that it's nice and declarative, that we're not mixing concerns. But at the same time, we can have the benefits of local JavaScript. We get to use modules for scoping or namespacing. We get to refactor easily. We get to compose things nicely. We can extract local-only components or helpers or modifiers. That's what I'm showing in this example here and I'm not going to walk through this in detail now because I'm going to walk through it in detail later. But this is a big win for both TypeScript and JavaScript users in terms of the authoring experience and it's probably the biggest change we've ever made to authoring components in Ember. I think it's going to be great. I've been using it a bunch on small things here and there and it's really, really good. It feels like the best parts of things like JSX and the best parts of how Ember's templating story have worked historically, it's great. But for TypeScript users, there's an added benefit and that is that Glint, our type-aware template system, doesn't have to reinvent the world. Previous language servers for Ember have done really great work but they've had to do that with heuristics. What is this component? I have some guesses but I don't necessarily actually know. What is this helper? Same deal. That's not true anymore. Glint can do one thing and it's a hard thing but it's just one thing and that is turn a template into something that TypeScript understands and after that it can let TypeScript do all the hard work. Go to definition, refactoring, docs on hover, you name it. TypeScript does that and does it well. We don't have to reinvent that wheel. The other upshot here is that that works for JavaScript users too because Glint is not a TypeScript specific tool. As is the case with most things in the JavaScript ecosystem, it works for both. That takes us to RFC 785, test helpers which build on top of these new first-class component templates and they let us simplify authoring of tests for TypeScript users but also for everyone else and it makes rendering work the same way in app code and in test code which hasn't been true historically. Here's what I mean. With first-class component templates you can just use tracked state. You can get rid of using this.set and this.get to do special sauce things in your tests. This.set doesn't work anything like it did in app code in the pre-octane era and in fact it doesn't work anything like anything in Ember does. When you call this.set in a template today, in a test today, that triggers the entire app to re-render immediately. Nothing does that so we've gotten rid of that. Instead you can import something like tracked object or write a class with some tracked state and then work exactly like you would in an app. You call render just like you would today except you pass it a template only component using first-class component templates and reference the state that's in your local scope and then you update that tracked state just like you would in your app and you await re-render a new test helper that works exactly like Ember's re-rendering does except in app code it's scheduled. In test code you're in control of when it happens and then you check, did that have the desired effect and spoilers it does. The win here is we're composing features together rather than having to do special sauce in tests. This all just works and it all just works for TypeScript users but it also all just works for JavaScript users. For TypeScript users notice that there's no monkeying with a test context here in particular. This feels good. Now we have RFC 776, TypeScript blueprints. Our goal was to keep our blueprints up to date always. Members of the TypeScript community in the Ember ecosystem will know that that is not a thing we have succeeded at historically. We need a single source of truth for our blueprints because otherwise they just will get out of sync. And we want this thing we do not to be something people have to re-implement over and over again but something that any Ember library or if you're interested, any non-Ember library can use for a blueprint system. And the how is that we start with TypeScript blueprints and if you're a TypeScript user we just hand those to you and if you're a JavaScript user we strip out all the types and then hand those to you. It turns out this is annoyingly difficult because all the tooling for these kinds of things in the Ember ecosystem, I should say the JavaScript ecosystem, speaks in terms of abstract syntax trees and we need to speak in terms of concrete syntax trees. So to be honest, it's a really clever pile of hacks to make this work but it does work. And again, it's the kind of thing that means we can move much more quickly going forward and stay in sync. The net is that you just type dash dash TypeScript when you're generating a blueprint and it just works. You can do this today on Ember 4.4 beta one or later and Ember CLI beta Ember CLI 4.3 beta one or later. You pass the enabled TypeScript blueprints feature flag and the TypeScript flag and when you generate a component class you get it in TypeScript if you pass the TypeScript flag. We're very excited to see this land on stable because we're very tired of trying to keep the component blueprints up to date. The last key authoring code feature is RFC748, the Glimmer Component Signature Type which is designed to let us fully represent what a component is. It's arguments, the element that it will apply the splash attributes to, if any and any blocks that it yields. And this provides nice hooks both for documentation tooling and for Glint to be able to type check things. The research we did in this also gave nice input for other parts of the ecosystem, modifiers and helpers for example. Our types already support the same kinds of things there now. Here's what it looks like in practice. We start by defining a signature interface and applying it as a type parameter to a component. Then within our interface we define what are the arguments? In this case a name which is a string and an action, set name which takes in an input event and we'll do something with it, presumably update that name. Then we can use those in a type safe way on the actual component class. In the greeting getter here we derive from the arg and our input is wired up via the on modifier to invoke that. One of the nice things here is this kind of thing also works for JS stock. So you can get autocomplete and support that way in just a regular JavaScript component by writing this kind of thing. We cover that in our docs. We also document the element. We're going to splatter butte onto a div element. So if you try to pass in a modifier that expects to be on an audio element, Glint will say, nope, sorry, you can't do that. And finally we document the blocks that we yield. In this case, a header which receives the greeting and it's a string. If you had a default block, you would just use the name default here. The key is we can now document the full API service of a component. We can extract that into docs and Glint can type check it. So that's our updates to code authoring. That's a pretty good set of things. And as I've said throughout, all of those benefit JavaScript users too which is one of the reasons I'm so excited about how we've been approaching this process. The last part of our story is a documentation update. And this one is far reaching because remember our goal was to make TypeScript a first class peer to JavaScript. Well, that means that needs to be true in our guides and our API docs too. We also need to coordinate this with the rest of our Polaris teaching story. As we make all of these features work together nicely and as we polish up the rest of the details that we introduced in Octane, we need to make updates here. This includes being able to have a toggle that shows TypeScript versus JavaScript. And that probably means different pros in those places. It probably means our API docs need to know what to do with generic types when presenting to a JavaScript user. There's a lot to figure out. There's design to be done. And so far it isn't done. So we could use our help. Come work on it with us. We'll mentor and support you through that. The learning team, the Ember TypeScript team and the Ember Framework team. That's where we are today. Where are we going in the future? Well, number one, we're going to finish what we started. We're going to ship great types with Polaris. Types have been good in Ember up to today but we're going to make them great. Second, we want to continue collaborating with the rest of the TypeScript ecosystem. Many of the things we've done here are very intentionally not Ember specific. We don't want to be siloed and we think that there's a lot of positives on outcomes to be had in collaboration between frameworks, between library authors and so on. So please kick the tires on a bunch of these ideas and policies and tools and let us know what does and doesn't work. Collaborate with us. Let's make progress together. I want to leave you with a vision of the future. What does it look like when we put all these pieces I've been describing together? All those RFCs we've written when they're implemented, how will it work? So we're just going to implement a fairly simple component. We'll import component from Glimmer component and then create a class backed component because I know we're going to need some state for an audio player. This is an example I mentioned a few minutes ago. And this audio player will take as its source an argument so that people can use this for lots of audio players. And that'll take a URL as an argument. So we'll write an interface, a signature which defines that argument. URL is a string. On thinking about it though, maybe that should be a source URL. So after we apply the signature, let's go ahead and rename it. That's a neat feature if we can make that work. We'll also specify that we'll splat our attributes and therefore modifiers onto that audio element. So if someone tries to pass a modifier that works only with divs, it won't type check. Now to actually handle play state, we're going to need some tracked states. So we import tracked from glimmer tracking and we define an is playing type variable on this class. And let's not play by default. That's not kind. We'll author then a couple of buttons and wire them up to that tracked state. These buttons of type button for good accessibility will connect using the on modifier. And when someone clicks it, we'll call a couple methods, play and pause. Now you might notice the red squiggles there on, on and this dot play and this dot pause. That's because we want those to type check. And in first class component templates, we need to actually import things like on for them to work. The upside here is that there's no guessing about what it is. We just use normal module resolution. Same thing goes for play and pause. We want glint to tell us these aren't defined yet and it will. We now use those to update the state of this dot is playing. Play sets it to true and pause sets it to false. And then we need to go ahead and connect those to the audio element. So we'll write a modifier. Modifier currently comes from Ember modifier, not at Ember modifier or at Glimmer modifier. We're working on fixing that, but this is what it'll be like for the near-term future. So I wanted to show you honestly. And that modifier will be a play when something is true. It'll take in an audio element and whether it should play just a boolean. And when it should play, we will call the element dot play method. And when it should pause, we'll call the element dot pause method. And then we'll wire that up on our audio element. We'll apply the play when modifier there. And if we forget to pass arguments, well, Glint will tell us. It'll also show us the signature. What are the arcs that it allows? What is the element it works on? So we pass this study is playing to it. It's pretty nice. I get really excited thinking about that vision of the future. But that's a trick. That's not a vision of the future. That's a vision of the present. All of this works today on the latest version of Glint, latest version of Glimmer Component and Ember modifier. Everything I just showed is real. And it works for JavaScript or TypeScript. That's pretty exciting if you ask me. It's happening. We're going from good to great. This has been the road to TypeScript. Thank you.