TypeScript 5.0 Beta Deep Dive

TypeScript 5.0 beta brings a bunch of exciting features, including const annotations, decorators, speed improvements and a new option for resolving modules in TS.


0:00 What's up wizards? TypeScript 5. Beta just dropped. It brings in a whole load of new functionality and new toys that we can play with. I'm going to be talking about the headline items and showing you exactly what you need to know for the upcoming 5.. Let's go.

0:13 To get started, you can add TypeScript at the beta version to your package.json. Then when you're inside a TypeScript file, you can run Command + Shift + P and select your TypeScript version. You should use the Workspace Version 5. Beta. I'm going to go quick fire through some of the small changes and we're going to see the big changes at the end.

0:29 They've added a new option here to moduleResolution called bundler. We had some options called node16 and nodenext before, but this meant that you had to add the file extension to things that you imported.

0:38 Using "moduleResolution": bundler now means that TypeScript is going to map up to your modern bundler like Vite, esbuild, swc, Webpack. This is an extremely welcome change, but probably, your framework is just going to handle it and it's all going to be fine.

0:50 Another small change they've made is they've slightly improved enums. I'm on record with my thoughts about enums, you can take a look here. Let's say we have an enum called LogLevel with DEBUG, WARN, and ERROR. We have a function called log which takes in a level and a message. In previous versions of TypeScript, you could pass any number into here and it would work.

1:09 TypeScript wouldn't give you an error message here, but now you can pass in zero for DEBUG, one for WARN, and two for ERROR, if you really want to. If you pass something that isn't represented by the enum, then it's going to error at you.

1:20 If you use literals instead, then this will have the same behavior as before. It will still force you to pass in LogLevel.DEBUG here, instead of the literal value. This is another extremely welcome change, but I don't think it changes my overall idea about enums, but you know, safety is good.

1:36 The other welcome change here is some speed optimizations in TypeScript itself. This means that the actual package size of TypeScript is now only 58 percent of what it was. If you're using TypeScript to build your application or your library, then you're going to see some speed improvements.

1:49 I imagine you're also going to see some speed improvements if you run TypeScript on CI to lint your project, which is what most people do. Without any changes, this is a massive win and I'm keen to hear in the comments if you found any improvements.

2:01 Let's talk about the two big headline items from this release. Number one is const annotations. Const annotations give you a new tool with generics in order to improve the inference that you get when you call functions.

2:12 Let's say we have a function called routes. The routes parameter takes in an array of T, which is going to be a set of routes. We create a function called addRedirect, which is going to redirect from one route to another route. Let's call this routes function with /users, /posts, and /admin/users, and we get back a router at the end of it.

2:31 Now, let's use the addRedirect function on that router to redirect from /admin/users to /users. This code doesn't do anything, of course. We could add some implementation inside here, if we wanted to, but I'm going to use it to explain how TypeScript infers this T.

2:44 You notice that even though we've added these three routes here, I can add anything to addRedirect and it won't yell at me. That's because if we hover over routes, you can see that it's inferred as an array of strings instead of the literal values I've passed in. Which means that addRedirect down here is from: string and to: string, meaning that it accepts any string.

3:03 This is quite a difficult problem to solve. There are some tricks to do it, but in TypeScript 5., you can just add a const T to the start here. Now, the things that you pass into routes are going to be inferred as their literal. We get /users or /posts or /admin/users. This means that addRedirect down here has from the route and to the route as well.

3:22 That means that when we change this, we're going to get autocomplete for the options that we have here. I can either add /admin/users or /posts or /users. Quietly, I think this is one of the most important features TypeScript has shipped for a while, because I'm starting to see how much easier it makes handling these generics when you care about the literal values.

3:41 Finally, let's look at decorators. Decorators have been around in TypeScript for a while under an experimental flag, but 5. brings them up to speed with the ECMAScript Proposal. Which is now in stage three, meaning it's in the stage where it gets added to TypeScript.

3:55 Let's imagine I create a class called SDK. I'm going to add a bunch of methods to this SDK, including getUser and getPost. I've stubbed them out with different methods, but you could imagine these would go and fetch the post and the user from a database.

4:06 Let's imagine that I want to log the getUser and getPost functions. I want to log when the method is called. I also want to log when the method resolves. For this, I'd need to make getUser async, I'd need to capture the result of Promise.resolve, add in a log, and then return the result.

4:20 I'm then going to have to copy this logic for every single method I have here. If only there were an abstraction where I could just wrap these methods and add some logging to them. This is where decorators come in. They allow you to wrap methods and entire classes in order to add functionality.

4:35 Let's create a function called log. It's going to take in the originalMethod, which is typed as a function that can take in anything and return anything. As a second parameter here, we're going to add underscore context, which is a ClassMethodDecoratorContext.

4:47 Next, we're going to return another function, which is going to be the method. Just like in getUser, I'm going to save the results here by calling the originalMethod using originalMethod.call. This lets us pass this into the originalMethod, meaning it doesn't lose the context of where it's called, in this case, in the SDK. Next, we can return the results.

5:05 Now, let's do the logging that we want to do from this decorator. Instead of hard coding getUser, we can replace it with _context.name. This is going to be the name of the method that we've called. We can replace the ID by JSON.stringify in the args that we get. Next, let's log when the function completes too. All the code that we need for our decorator is complete.

5:23 Instead of having all of this complicated code down here in repeated code, we can just decorate the functions with log. When getUser is called, log will be called and it will call the underlying method itself. This means you get a really beautiful descriptive syntax, which can abstract away some horrible complicated code.

5:41 There's a lot more to talk about with decorators and I think I'll do a separate video on typing them properly, because as you saw, there was quite a lot of any's in my code. I'm really excited to see where they go and I feel like this might rejuvenate classes in the JavaScript community a little bit.

5:53 Since function components took over React, classes have been on the out. Being able to capture abstractions and decorators seems like a really, really nice feature for TypeScript and JavaScript as a whole. I'm excited, bring the classes back. Let's have them. It's been lovely seeing you as always and I'll see you very soon.

TypeScript 5.0 beta is out! Here's a breakdown of all the most important features.

Discuss on Twitter

More Tips

Assign local variables to default generic slots to dry up your code and improve performance