Building Familiarity with TypeScript's Syntax and Functionality with Orta Therox
Orta Therox, a former member of the TypeScript core team, shares insights into how developers can become more familiar and productive with the language.
Orta covers generics, compiler flags, the
lib.dom.d.ts file, and more while emphasizing the importance of consulting the TypeScript Handbook whe
Matt Pocock: 0:00 What's up, wizards? I am here with Orta Therox, a TypeScript legend, ex TypeScript contributor, and TypeScript brain hiding beneath a flat cap. I'm extremely excited to talk to you about all things TypeScript, pick your brain about your mental models for all sorts of things, get you to talk us through some code.
0:20 You're one of the people that helped write the "TypeScript Handbook" too, so understanding your models for how you teach TypeScript and how you understand it on a deep level. I'm really excited. Could you introduce yourself to all the kids at home?
1:10 I started to devote quite a considerable amount of time to getting TypeScript up and running within our team. Then that eventually turned into a job working on the TypeScript compiler, where I spent about two and a half years working on documentation, and integrations with external systems. I started bootstrapping Svelte's documentation.
1:50 Really, it was a little bit of compiler work, but a lot of outer integration systems. When I was interviewing, I pitched to the TypeScript team that I'm not a very good compiler engineer. What I can be is somebody that can work on the moat of TypeScript if TypeScript is a castle and the village that surrounds it.
2:10 TypeScript is trying to scale that project, it constantly requires rethinking about how to interact with the rest of the world. I was hired to work on that.
Matt: 2:21 That's interacting with other systems, interacting with developers, and interacting through documentation as well. I think a question I'd love to start you on is at a high level, how would you explain TypeScript to someone who's never heard of it or someone who's maybe heard a little bit of it but is a little bit skeptical?
3:14 We often talk about the types as being the key feature of TypeScript, but internally a lot of how we frame it is types for tooling. That is the types that we give you need to be able to power really useful tools in order for people to feel compelled that the advantages of using TypeScript are the tooling and the types are there to get you in that space.
Matt: 3:52 Interesting. A lot of what I'm focusing on with Total TypeScript is taking folks from a beginner level with TypeScript into places where they can build their own abstractions, where they can feel confident with understanding the flow of types through their system, and especially working with generics.
4:14 There's a lot there in that topic. How would you explain generics to someone who'd never heard of them before?
Orta: 4:24 I think generics are pretty easy to evolve when you think a little bit about how in variables work inside normal expressions, normal code, if you will. Generics is a way of providing arguments to some other type. You might have an array type and you want to say what is inside that array type. You'll add what is effectively a parameter to the array that says, "Hey, this is the thing that goes inside it."
4:53 That, to me, is usually the first way of introducing generics to people as being, "You already know there's a variable associated with this, but you call it an array of strings and your variable name. Why not take the of strings and put it into the type? Now the type says, 'This one generic parameter ensures it's only a string on the way in.'"
5:14 Defining it as the inputs is usually the first way to think about how generic arguments exist in TypeScript. Once people feel confident with that, then you can start talking about, "Hey, the generics can now affect the output, right? You can pass this type in. It gets transformed a little bit. On the way, it comes back out."
5:37 You can talk about how your array type of string now is used, that you only put the string out on the return value. That is the nice way to teach it, in my opinion. You first say, "Look, this thing can be used as an input." Then later you can say, "This thing can be used as an output." Then when people are really wanting to understand it, then they're, "How do I do parameters?"
6:01 I don't need to add, "By having defaults," or, "How do I constrain those parameters?" That's when things get more complicated, obviously. It's an incremental way to learn. A parameter is simple. Then slowly you learn more and more of these features of generics that get you to more and more powerful positions but at the trade-off of more and more complexity.
Matt: 6:22 Why don't we get straight into a bit of that? I think that's a really nice lead-in to start looking at some code, if that's OK. I've got a function here, which is object keys. What this does, it basically says, "Imagine we have type object to array of keys. We have a type of obj, or let's say, result equals object to array of keys." You pass in a 1B2, for instance.
6:49 What you're going to get out of this is an array of these keys, which is A or B here. Then object keys down here, it basically does the same thing, it maps the type helper onto a function. When you call it, then you get object keys. Let's say, we call this at runtime A1, B2. How would you describe the difference between these two constructs?
Matt: 8:13 Interesting. Style choices, in what sense?
Orta: 8:19 One of them exists inside the type system entirely. The type object keys...It says that...[reads quietly] the array of those. Then that's just using it. The one on the bottom is just defining it again at runtime. What is the difference between the type of than that you're just declaring them?
Matt: 8:47 I'm trying to get to an idea that one of these, this first one, as you were saying, it's basically, you just pass in a type as an argument. There's no inference happening here. Whereas, in this one, when you use these type object keys here, there's this inference happening. You're not actually having to pass the parameter in. You can pass it in. You can pass it in a number, B number.
Orta: 9:14 Yeah, sure. I see what you're getting at. The interesting one is that the first one is really just an ephemeral type. We can start that again if you like because I definitely was hitting in those directions, but I would definitely have veered off in a different way.
Matt: 9:31 Cool. Let me do that again. Let's start looking at some code here. We've got an object to array of keys here, which is like a type helper. It's saying, "We're going to take in a type into here. Then we're going to return an array of key of T." When we pass in a 1B2 into here, then we end up with A or B here.
9:57 Then this here, this object keys is a function which does the same thing but it's mapped onto a function here. This means that you don't actually need to pass in any types when we use it. It's basically just, we infers it from the type of the thing that's passed in. I'd love to get your thoughts on the differences between these two approaches and how you would teach this yourself.
Orta: 10:28 Some of the key distinctions between both of these is that the first two examples, like the first two types only live in the type system entirely. In theory, you could reuse those in different contexts and in different places.
11:06 The usage side of the API would mean that it is automatically inferred, simply because when an object literal is passed initially into a function, then that will get passed in as your T. If you do not pass that object literal in first, then it will be unnarrowed, and you would actually lose a good chunk of the initial inference that you were hoping for.
Matt: 12:23 You've like to take this all the way back, then. Imagine if you just take all of the clever type annotations off this, and you've got a function that's basically just wrapping object or keys. Then what you end up with here is just string array here.
12:39 By adding all of this clever stuff, you have an opinion about what that type should be. You're getting TypeScript to infer this type here from the thing that you pass in. Then, you're annotating extra stuff on top of it, basically mapping the type helper onto the function. Yes.
Orta: 12:58 There's two ways of doing it. They're different. One is reusable at a type level. You can use object to array of keys and other functions now if you wanted to, but object keys is really only useful if we're in the context of object keys.
13:18 There's a TypeScript feature that came out in maybe TypeScript 4.6 that allows you to set up an object keys, like the instance evaluation with a set type ahead of time.
13:32 You can actually reuse that object keys with a set type, with a single generic parameter, and then later reuse it. Realistically, those one is used for reuse in the first case, but then the second one is used for...You're only going to have one function called object keys in a code base realistically looking at that. That's probably how you probably should be building it.
Matt: 13:58 Got it. That's really interesting. What problems do you feel that this kind of generic code is designed to solve? What would TypeScript look like without generics?
Orta: 14:14 I don't think you can't make a type system that is as complicated and feature rich as TypeScript about generics. It's just like, as a structural type system, which means a type system that checks every single field to see if they match the other thing that it's been compared against.
14:34 They just come to a point where you need the ability to reuse fields in some way. If you didn't have that, your types would be incredibly long and incredibly large amounts of overlapping code between each other. Generics gives you that reusability that is essential to build any complicated type system.
14:58 I say that, but I used Objective-C for about 10 years, and it does not have generics. It has a thing called lightweight generics. We got by, but you needed a type system in that case that's nominal, wherein every single class has a unique type that if you compare a type of an animal to the type of a dog, those two are always different. Whereas in TypeScript, they could be the exact same, and they could be treated the exact same in different cases.
Matt: 15:28 Got you. Let's use that as a segue to dive into talking about TypeScript's sort of....I don't know what you'd say, structurality. It's a structural type system, right? Not nominal?
Orta: 15:40 Yeah, yeah.
Matt: 15:41 I've got a little sample here, something that I was working on for an article recently. I'm really interested in your thoughts about this error here. Let me actually just print this error out. Now, if I pull this out here.
16:00 I'm interested by this error, because what's happening here is we've got an example function at the top, which is a type. It's declaring that it's a function with something, with awesome being a number. I've actually made an error here where awesome is 1. If I change this to be a number, then the error is going to go away.
16:21 Now, I'm really interested by the structure of this error and what this tells me about what TypeScript is doing under the hood structurally. I'd love you to explain that at a deep level, if you could.
Orta: 16:34 Yeah, sure. TypeScript is a structural type system that is literally where we should start this conversation. What a structural type system means is that basically whenever we are sort of type checking, what we're really doing is checking if these two objects can be assigned to each other.
16:55 Can exampleFunc be typed to be called exampleFunction? That check actually happens where you are seeing the red squiggles right there. The way that type checking works at a very high level is that, first, we create these things called abstract syntax trees, which are an in memory representation of a source code.
17:21 TypeScript iterates through all of the sort of root nodes of that tree, which are basically what we call statements. Each statement is a line of code. We'll echo that a little bit, but effectively it gets to a point...
Matt: 17:34 This is the statement. This is the statement. This is the statement. This is the statement.
Orta: 17:38 Yeah, you have statement on that page, basically. We go in and we look at this identifier, right? ExampleFunc on the third statement. It is declared into TypeScript by that colon that is going to be this other type. TypeScript will look at those two and say, "I would like to look at the structure of the results of running this equals sign on the other side to the types that you have defined after the colon."
18:08 We're looking at, is example func the same as the function definition that is available directly after it? It's those two things that are being compared. What TypeScript will do -- again, I'm waving my hands a lot for a lot of this, but it's good enough -- but this looks, "This thing that we've got here that you have selected, the function, is that even a function? Is it function-like?
18:48 They compare the functions, and they say, "They're both functions, so we'll go in." Then it compares the arguments, "That's very complicated, so I'm going to completely gloss over that," but the return values of those is very simple. There's only ever one type of return value, and you can compare return values against each other.
19:08 It is now starting to compare, does the return values' structural match with the structural match of the types one? It will look at "something," and then it will look at "excellent," and then it will look at "awesome."
19:21 That's why when you saw that error, you started to see this breadcrumbs at the bottom when you had the full-on. Where it was like, "Something.awesome are incompatible with these types." The first line of the final part of the error message.
19:35 What that's doing is effectively looking...We find out where the break is, where the types cannot be assigned to each of them, and then keep going back and seeing which parts are broken. That eventually turns into the breadcrumbs that you're seeing in there.
19:49 TypeScript has to go all the way down to find the part that's definitely broken, and then it starts going all the way back up to say, "What is the closest path to an assignable type?" What parts are assignable? What parts are not assignable? You keep narrowing down until you find the bit where they do not assign.
20:07 That's where you start your error messages, and that's where you give you breadcrumbs, and then you eventually go back to say, "What was the definition? What was the part that said these two things are the same?"
20:19 That's where you give your error message. That's where your error message lives, up here on exampleFunc, but your actual error, per se, lives down at "awesome, 1."
20:28 Like if TypeScript was going to give you infinite squigglies and had different definitions of how we describe them, then it would be like awesome would be a bit that would be highlighted. What about this idea? I'm throwing it out there, just at this moment.
20:44 If you highlighted that exampleFunc, if your cursor was there, why not show a cursor underlines on all the related, we call them spans, all the related spans for where error messages should also be. Your mouse should be there, so awesome should also be highlighted now because that's where the 1 error message is. This is all under the block of what we call assignability bugs.
Matt: 21:12 Got you. What you're saying then is each line of this error message it's as though it's walking down the tree each time. It's like a stack trace, right?
Orta: 21:23 Yes.
Matt: 21:26 Because if we change this slightly to say, imagine instead of this being an ExampleFunction, we're actually just going to type the return type instead, ExampleReturnType, and then return it here.
21:41 This is a slightly different case because we're not saying the exampleFunc is the thing that we care about in terms of assignability. It's now this that's what we care about. Could you elucidate the difference here?
Orta: 21:59 At this point, it doesn't matter what the type of the actual function is. It only matters the type of what the actual return value is because that's the only place where we're actually telling TypeScript, "Hey, these are the assignability rules that I'm assigning to this bit of code."
22:13 You can actually see now that the first time that there is actually an assignability break is down at awesome instead of actually up at the return type. You're getting your error message down here because we know it's only at one place that we don't have to go further back up the tree to give you a 1 idea about where it is.
22:33 There's a good argument that both of those could have an error message. At the same time, if you know exactly where it's broken and it is exactly in one place, there's not much need to separate out the error message up here and the error message down here. You know exactly that it is awesome.
22:50 This can be a little bit tighter on the error messaging to tell you exactly where your error message is rather than tell you this was your assignability problem, but this is where the actual error message is.
23:01 In moving the error message, we have a tighter type system now in theory. Obviously, the function bit not being there does make it weaker in other aspects, but your error messages are a bit tighter simply because you've added more annotations at the right points.
23:16 Sometimes when you're working in very large code bases, trying to figure out how to have less TypeScript but more explicit in the right places can often lead to extremely good error messages in comparison to giant trees of like, "This bit to this bit to this bit to this bit are not assignable to each other."
23:35 That is definitely an art form that you really need to see a lot of error messages to learn where the right things are for that.
Matt: 23:44 Absolutely, because it's doing the assignability checkup there. It has to not only figure out the cause, but build up like a legal case to say, "This was the cause, this was the cause, this was the cause," going all the way down. The closer you can get to comparing an object to an object, I guess, is pretty good, whereas a function to a function is less good.
Orta: 24:06 A really good way to expand on this is I've been writing my own GraphQL APIs for a very long time, and people tend to build these GraphQL API DTS files generators that cover the entire graph of your GraphQL API.
24:24 If you don't know what those are, all I'm saying is making giant DTS files that have a very large amount of generics inside them. You get these error messages that are incredibly long, because it has to reference a ton of generics to eventually get to a single name of the query you're making.
24:41 A quick rewrite of some of those DTS generators, which I did recently, that just do exactly the objects that exist, the error messages went from being seven or eight sets of nested things deep to just being one, and your error messages became trivial. It required saying, "Hey, I want a simpler set of DTS files," even if there's more text there.
Matt: 25:03 Fascinating.
Orta: 25:04 It's a constant trade-off.
Matt: 25:06 This takes us into an interesting discussion, which is another thing I'd love to touch on with you, is around interacting with external libraries and external DTS files, because this can be really painful and can generate some enormous errors.
25:23 Another thing I want to focus on with Total TypeScript is giving people the confidence to explore all of the generated d.ts files that ship with TypeScript that come from types node and exploring those. I'd love you to give us high-level overview of how that works with TypeScript, like where DOM typings come from, all that stuff.
Orta: 25:48 I'd say there are roughly four spaces where types can come into your projects. There is the types that get shipped with TypeScript. We would refer to those as lib.d.ts. There's the DOM types, which .dts. There's Definitely Typed. Then there's included inside your either code base or your node package manager, node packages that already have .dts files.
26:41 Somebody has to ship those to describe that type system further down the line. That is what target does for tsconfig. It defines how far in the lib.d.ts do we add these types into your type system.
Matt: 26:54 Could I interrupt you so we can try that out?
Orta: 26:57 Please.
Matt: 26:57 That would be great, because what you're saying is, let's say, const whatever equals new Map. Going from this, how would I say, "OK, I want to see where this d.ts file is coming from"?
Orta: 27:14 Are you on a Mac? You could command-click on that, and it should show you, or control-click.
28:06 If you've got ES2017 installed, then it also includes all of ES2015, etc. The target definition in your TSConfig defines what is the year that you start, and then it goes all the way back in those. ESNext. That's all of them. That's the specs. We're just saying all of them.
Matt: 28:26 If I change this to ES3, then I'm going to get a ton of different errors because, let's say, map might not be included if I restart my TS server.
Orta: 28:38 If it doesn't, there's some magic going on.
Matt: 28:40 Of course, it didn't go the way we wanted to.
29:31 Lib.dom.d.ts is pretty complicated project that outputs a single DOM file and that small iteration file that you saw next to it. This one is a separate repo from the TypeScript repo. It is maintained by an outside collaborator who works at Mozilla, and is constantly up to date with spec changes in Web browsers.
29:58 The problem with this one is, realistically, you don't want to version your Web browsers in the way that you might for your language. There is some advantages to this. In the Babel world, you'll often see people say, "I want to use this version of a minimum two years support" and stuff like that. TypeScript updates once a quarter.
30:20 Every single version of the things we ship ship to everybody. There's a lot of incentive to just only do the latest. Our rules for this are, if a feature is in the Web platform in general, and you can see it in at least two browsers, that's Firefox, Safari, or Chrome EOMs, it has to be in two of those to be allowed in.
31:19 One of the few big TypeScript features I shipped was the ability to have a custom version of this and just you have a custom version of any of your lib.d.ts files as well. That can be overwritten by your package manager. It's a rare feature that people need. When people need it, it's there.
31:38 Some people want a different version of the dom.d.ts. Our rules of accepting something that's on two browsers, in my opinion, helps keep the Web a little bit more equitable. For a lot of people that are building Chrome-only, which is massive, they want a lot of extra APIs that are just not available in here, or not described in here, more importantly.
32:01 They exist in the runtime, but they are not described in this dom.d.ts. It gives them the ability to say, "Hey, I want a Chrome-only version of this dom.d.ts, which gives me my APIs." Whereas we still get to say, "Hey, we're doing the Web platform, not necessarily the Chromium platform."
33:01 These types are complicated. The React types are complicated. The React team don't use TypeScript. There are a set of absolute heroes that try to map this very complicated system of React into a runtime that can exist inside the type system.
33:24 It's not fair to call it third party because the React team are very involved, because if this doesn't accurately respect their thing, their runtime, it will absolutely cause them loads of pain because they'll get all these issues. They try and make sure that when they introduce hooks, there was type definitions for those straightaway.
33:47 Some library authors do that themselves. The React team allow it to run on DT. Running on Definitely Typed means that there is an authentication system in place, there is checks that it is not broken. It gets verified against TypeScript. A TypeScript compiler on its test system runs the newest versions of TypeScript every day against every existing Definitely Type project.
Matt: 34:39 This is where when you say, "OK, I want to use this library, this abstraction," you say, "I want to bring all of these types into my project." How important do you think it is for folks to understand the complicated syntax that's happening here?
34:57 Just to understand this function, you need to understand function overloads. You need to understand generic constraints, passing generics to other types, tuple types. How important do you think it is to understand this stuff?
35:49 I think that that is a spectrum that you start off just maybe making a map with a string as its parameter, but eventually -- and that's application-level TypeScript -- and you make something a bit like the types that we described earlier with generic parameters, extended something. That's library-level TypeScript.
36:09 You're making a generic function that's reusable anywhere inside your code base, so it needs to have a bit more TypeScript complicated stuff. Yeah, this one. You have to learn some concepts probably to start writing code like that if you've just written TypeScript as an application developer for a while.
36:29 Then if you want to start describing some of these really complicated things, then you really do just have to sit down and go through first a handbook.
36:38 One of the best ways of learning that is we did the series called...they were Halloween-themed playground exercises. [laughs] Did it two years in a row. They were good. Their goal is to actually teach you to get towards that types. By doing it incrementally by, "Here's how so and so works."
36:59 Eventually, we force you to read enough documentation to eventually complete some of these challenges. That's what type challenges are useful for, to get from this to the hard stuff.
Matt: 37:10 Definitely. That's the structure of Total TypeScript as well is building, basically. These challenges just progressively getting harder and harder. You said there was one-fourth thing, this one-fourth way that you could...
38:12 It's very traditional in terms of that setup for describing frameworks and libraries.
38:39 That's really hard to infer what the types are or infer what the return names are and things like that. It tries. That's what you got to do.
Matt: 38:49 It's a tough problem. Let's finish up then by talking a bit about...Focus gone. Something's going on. Let's finish up by talking a bit about TypeScript's future and potential places TypeScript could go. I think that's a hopeful note to end on. There's a TC39 proposal to bring types as comments, which is interesting. You're interested?
40:31 There is a culture of making sure that never feels like it's happening. The biggest one for me, for why I wanted to work on this is that types as comments allows other people to exist inside the tooling space and the language runtime space that is currently absolutely dominated by TypeScript.
40:53 Basically, the key definition for what the proposal proposes is that we declare areas of syntax where more or less anything can happen.
41:19 Right now, if you want to build Vite, you have to build Vite with support for TypeScript, because everybody's going to be throwing TypeScript code into it and it's expected to handle it, but Vite doesn't care about the types. It's not doing a type checker or anything.
41:54 You just know to ignore that area of code and TypeScript support is a matter of adding the .ts file support. Eventually, this is me extrapolating, but it would be very reasonable if this proposal passes all the way to start seeing a move away from .ts files and be .js files.
42:56 That's why it comes up regularly at the moment that people talk about enums in TypeScript and how they don't like them. I think even you had a video on it recently. TypeScript were not [inaudible]. It's a useful tool, but it's not the tool that TC39 have said is what enums should look like.
Orta: 44:06 Exactly. I was on a call with Anders last week and he still was saying that.
Orta: 44:11 He's been saying that for 12 years. Yeah...
Matt: 44:12 Well, it's still crap or like it's...
Orta: 44:16 [laughs] No. I mean, there are other people doing it, but we're the only ones that are really doing it. If you get what I mean. You can't replace TypeScript to VS Lin.
44:27 You could replace TypeScript with Flow, but Flow is at a point now where it understands that it wants to be only working on the Facebook codebase. You can use it, but that might not necessarily give you what you're looking for.
Orta: 45:05 Yeah.
Orta: 45:16 Oh, are you implying, are you leaving me somewhere?
Orta: 45:24 Of the whole situation what it is?
Matt: 45:26 Yeah.
Orta: 45:30 I guess I got two angles for this. The first is, those decisions were made a decade ago and those decisions were made in a completely different environment. I agree with the decisions they made at the time, especially considering some of the contexts in which they were made. Maybe a good way to frame that is a heart.
45:47 The second part is a concrete example. Decorators have been experimental in TypeScript for about eight years. They were initially added because the Angular team and Google were thinking of making their own programming language called AtScript, which was going to be like TypeScript but have decorators.
46:08 TypeScript and the Angular team came together and said, "Well, if we add decorators to TypeScript, would that be enough? Then we only have one language between those, and we're not trying to step on each other's toes." When Angular came out, there was like, "Hey, we're working with TypeScript to make these decorators in there."
47:00 Namespaces was wrong. It was not where the ecosystem went because eventually, we got a 1 module boundaries through import, exports, and the concept of a module script type in the spec. Then, for enums, the proposals for enums do take the TypeScript one into account, so it's very unlikely that they will truly break all your code when and if it does come out.
48:12 Namespaces can be impossible to replicate in some cases because there's just features in there that just don't exist anywhere else. No one else is trying to add types to lodash and things like that. The features of namespace is built to do that sort of thing, too. I recommend against them.
Matt: 48:34 This has been an amazing conversation. Thank you so much for diving deep into code and me showing you stuff although you weren't prepared for your answers so eloquently. This has been so fantastic to actually meet you and talk to you as well. It's been brilliant.
Orta: 48:50 It's been a pleasure.
Matt: 48:51 Thank you. Awesome. Thank you so much, Orta. Where can people find you?
Orta: 48:57 I'm a Mastodon person nowadays. I've joined the cool kids in the Vue crew of Web tools, so web2.ls/orta.
Orta: 49:11 Yeah, I know. I feel like as an old-school open-source person hanging out with all the Vue kids, I'm like, "Wow, you guys are doing awesome work. I just don't understand what you're doing anymore."
Orta: 49:25 My Twitter will redirect people there.
Matt: 49:27 They do seem to be really young, actually, and extremely enthusiastic and really nice as well.
Orta: 49:33 They've got the energy that you only have in the first five years of open-source content. After that, you get really like, "No, I'm not doing that." [laughs]
Matt: 49:41 You're 12 years in, right? Yeah. You must be off a cliff now.
Orta: 49:44 Yeah, or I call it defensive open source.
Matt: 49:47 Lovely. Thanks so much for this conversation. I really appreciate it.
Orta: 49:53 Nice. Chao, folks.