Gabriel Vergnaud on Type and Value Level Mapping in TypeScript
Gabriel Vergnaud, the author of Type Level TypeScript and the ts-pattern
library, joins Matt to discuss how TypeScript's type and value levels map together.
He believes this perspective can help people solve their typing issues. Inference is also key, as it allows for types to be derived from oth
Transcript
Interviewer: 0:00 What's up, wizards? I'm here with Gabriel Vergnaud. I'm super excited to talk to you. You're the author of a course called "Type-Level TypeScript." You're author of TS-Pattern as well and general TypeScript wizard. I'm so excited to talk to you and unpack your mental models for TypeScript and how you came about this knowledge and how you use it to build the cool stuff you build.
Gabriel Vergnaud: 0:27 Yeah, I'm...
0:28 [crosstalk]
Interviewer: 0:27 I'm so excited you're here.
Gabriel: 0:29 to be here. I'm a total fan by the way and it's great to be here.
Interviewer: 0:33 Awesome. I'd love to just kick off, what was your journey with TypeScript? What took you from where you were to where you are now?
Gabriel: 0:43 That's a good question. Like many people, I guess, I started with JavaScript. I started as a full stack JavaScript developer back in 2014, I would say.
0:59 At the time, TypeScript was not a thing, even though it already existed. I worked on like very, very large codebases earlier in my career, like JavaScript codebases. It was not a great experience. Everything was breaking all the time, because we were making incompatible changes with colleagues without realizing it.
1:22 I think that's part of what got me interested in other languages initially, strongly-typed languages. I tried a lot of different languages, but one that I found particularly interesting was Haskell, which is a very niche language with a very strong type system.
1:47 I liked a lot of parts of it. Pattern matching was one feature that I liked. This is this idea of being able to do code branching in a declarative way. Instead of doing if and else statements, you basically ask, "Is my data matching this pattern? Does it have this property? Is it like this nested structure, etc.?" It's really nice.
2:18 That's why I started this side project of creating a pattern matching library for TypeScript. This was a little bit after, but I got interested in TypeScript. I liked having a type system and stuff, but I already missed some of those features.
2:34 I started this library and I didn't expect it to be like a success or anything. I didn't think anybody would use it. This was a cool hobby and some experiment to see how far the type system could go.
2:51 I actually discovered that it could be really, really far. It could go very, very far. I realized that the type system of TypeScript was actually a programming language. You could apply the same concepts that you apply every day in JavaScript, like loops, declaring variables, parameters, functions, and all of these things that we do all the time, code branching to the type-level.
3:19 To a large extent, you could really reuse this knowledge at the type-level. Just the only thing that makes it a little tricky is that the syntax is very, very different. That's where the idea of creating the Type-Level TypeScript course came from. I really wanted to expose this and try to show this mapping between type-level concepts.
3:46 I call type-level, basically any code that's in a type, after the type keyword essentially, and the value level. It's everything that you can do in JavaScript. Show how those things map together so that other people could realize that and make this shift of perspective that will help them solve the typing issues and build a more robust mental model for the type system.
Interviewer: 4:15 That's really interesting. You talk about the type-level and the value level. That's the terms you use. Let's say, we take the type of keyword in TypeScript that can take something...Is it right to say that it can take something from the value level and turn it into or bring it to the type-level? How would you explain that?
Gabriel: 4:35 Yeah. That's exactly 1. Those two words are...let's call them different words. You have the world of types and the world of values. For the most part, they are separate. You can't really cross this boundary. There is one way and one direction in which you can cross this boundary.
4:57 It's going from the value level to the type-level. That's called type inference. That's what TypeScript does all the time. You declare an object type. It has two fields, a name and a age property, for instance, which are respectively a string and a number.
5:15 TypeScript is going to be able to infer the type of your object for you. It's going to lift these objects that you created from the value level to the type-level. That's what the type of keywords does. That's interesting because it would be great actually to be able to do the opposite, go the opposite way, and turn the type into a value.
5:39 I get this question all the time in the TypeScript Discord. People have a union type. They want to generate an array from that. My only answer is that the only way is to have some build tool that would generate values from your types. It's not great. Inference is the way to go usually. You want to start by values and infer types from them in TypeScript.
Interviewer: 6:04 What benefits does knowing that have for the structure of your types, let's say? I imagine before, in less interesting dynamic type languages, you would have to declare a lot of types. You would have to say, "This type has this fields. Then you have this type that has this fields."
6:27 In TypeScript, we get to do something different to that, which is we can derive types from other types. What's your mental model for that? How useful is that in application code?
Gabriel: 6:41 It's extremely useful. I would say that the only types that you are 100 percent sure are correct is the types that have been inferred by TypeScript [laughs] because they directly map to your code, right? They can't be wrong. They can be imprecise, that's 1, but they are not wrong.
7:05 If you start from there, you can start thinking, "How do I enhance the type safety of my project?" One way is to try to declare as few type as possible and try to infer everything from your values. If you push this idea, it means that you will need to transform these types that you inferred from your code into something that's more manageable.
7:32 Maybe you want to reuse something. You want the source of truth of your types to be coming from this object definition in that file. You want to reuse that in this other file for the parameters of this function. You also want the parameters to be a little bit different -- you want to add this property, you want to remove that property, etc.
7:54 That's why being able to transform types is super-useful. It's so useful. We use that all the time in [laughs] TypeScript. There are a great set of utility libraries provided by TypeScript, things like Omit, Pick, Extract, etc., that are pretty well known. You can also build your own.
8:17 When you build your own, you essentially abstract over some common patterns. Maybe you noticed a pattern in your code, in your types, you are always doing the same transformation. Well, you can just put that into a function. This function just happens to be called type. So we call the generic type in TypeScript.
Interviewer: 8:35 That's interesting. Touching on that, you said that inference is the best way to extract out your types, definitely, instead of manually declaring them because they always represent what your code is doing. How do generic functions map onto that idea?
9:00 What use cases do you see for generic functions in application code, and how do they help you infer types instead of re-declaring them?
Gabriel: 9:09 If you create a function in TypeScript, you will have to declare the types of its arguments. It's actually the only thing you need to declare that should be able to infer the return type from the code. Arguments needs to be declared. You have, basically, two ways to declare types for your parameters or arguments. I use those terms interchangeably. I don't think it's very important.
Interviewer: 9:37 Yeah, me, too.
Gabriel: 9:38 Either you give them the types you want them to have. If you do that, you are going to forget about the types of the values that your caller passed through your function. It's easier if you've got example. You are going to lose some information in the process.
10:02 If you define a function as a generic function with type parameters, you put angle brackets with, let's say, T, you declare your type names and use that in your argument, you don't know exactly what this type is. You know that the caller will preserve this information when they are calling your function.
10:28 It's important because if you are, for instance, defining a function to transform an array and you don't exactly know what type is inside of your array. You know how you want to apply the transformation. If you type this array as an unknown array, then you will return an unknown array, and it will be just unusable from the caller point of view.
10:54 If you declare type parameter, you can map that to a different type parameter and the caller of the function gets to decide what type this is. It makes the code much more reusable.
Interviewer: 11:08 The idea is that generic functions remember and non-generic functions forget things, or they can't see certain things. They can't see the links. You're telling them how to get the links and telling the inference how to work. That's really nice.
11:23 I'd love to jump to something else now, which is you've got this absolutely wonderful metaphor for unions and indexing in TypeScript that I really love. I would love to maybe jump into some code if you'd be up for that or whatever you [indecipherable] . Should we jump to the code?
Gabriel: 11:42 Yeah, let's go.
Interviewer: 11:43 Yeah, let's do it. I'd love to just get your thoughts on all of this.
Gabriel: 11:53 In TypeScript, you can declare object types. We do that all the time, like this user type at the top. You can also use this square bracket notation to retrieve the type of the property from this object type.
12:13 If you want to transform...let's say, this user type has been inferred from an object, you might want to access the type of one property because you don't really know it in advance since that's been inferred for you. You might use a page here and you get back the type number. If I try name, I get back string.
12:36 Until now, it's pretty similar to what you can do in JavaScript. Here is where things are a little bit different. You can also read all of those properties at once. What you get is a union of the types of those values. That can be a little bit counterintuitive because it doesn't really map to something that exists in JavaScript.
13:05 We know that, for instance, object types map to JavaScript objects, tuple types map to JavaScript arrays. Union types, what do they map to exactly? To answer this question, we have to step back a little bit and try to understand what union types represent and how do we create them.
13:26 There is no way to create a union value like this doesn't exist because union types just represent different possibilities your code might handle over time. For instance, let's jump to the second example in this file. Let's say I'm walking to the office.
13:50 I really want a coffee, but I'm running a bit late. I have to make a choice. I see a coffee shop. Do I enter and grab a coffee, or do I just go straight to the office? As a person, I'm going to make a choice. I'm going to decide. If I grab a coffee, I'm going to be late. If I keep walking, I'm going to be on time.
14:15 If you map that to some types with code and you have a function that takes this action as a parameter, from the perspective of TypeScript, all of these possibilities exist simultaneously. It's like you have different parallel universes. Everything that can happen happens. When you define a union type, I like to say -- I know it's a bit silly, but I find it funny -- I like to say that you create a multiverse and suddenly those two things exist simultaneously.
14:50 From the perspective of TypeScript, it's really what happens because this action, since we don't know what it is, we can just pretend that it's both. If it's both, it means that everything that you do with this value need to be evaluated for all possible cut paths that you are defining.
15:07 That's where this idea of multiverse becomes interesting because sometimes you will have one value, and then it branches out. You have a bunch of possibilities. You create new possibilities Then all of them generates new and new possibilities, more and more possibilities. Sometimes everything collapses to a single version of reality because it turns out that your code isn't doing all of these possibilities in the same way.
15:36 That's where this analogy, I think, is useful. This happens with object types. Here are the transition objects, which basically transform this action to the final state I'll be in. Here the results is late and on time at the same time. It's a bit complicated. It also happens in other contexts.
15:59 If you use a conditional type with a union type, it's also going to happen. If you use a union type in an interpolation of strings, like a template little types, as TypeScript puts it, it's going to happen as well. You always have to remember that union types represents different versions of reality. That's a single thing. That's why this happens.
Interviewer: 16:26 That's an awesome explanation. The idea here then, when we index into this, if we think of this as the start of the story is here, basically, in name or age. Then because this story could go in one of two directions, it could either be name or it could be age. We use both of those to index into user.
16:49 In both stories, they go that way. We end up with string, which is the name story. Then number, which is the age version.
Gabriel: 17:00 Yeah, the words, it's like you were doing user of name and user [indecipherable] .
Interviewer: 17:09 Got you. Exactly.
Gabriel: 17:13 This evaluates to the union [inaudible] .
Interviewer: 17:16 Which is the same thing. Of course, then if we did something like user, Keyof user, then this would be the same thing, except we're just now...How do you think of this in your mental model in terms of Keyof producing all of these different possibilities?
Gabriel: 17:32 Keyof gives you the union type of all keys in an object, right? This union type is basically all the possible cut branches that could occur if you take the key of an object. If I'm writing a get function, it takes an object, it takes a key. I could be passing any key to this function. It has to support all of these different possibilities of all of these different versions of realities.
18:02 From the point of view of get, of the body of get, inside the body of the function, it's like all of those possibilities were happening. That's how I think about it.
Interviewer: 18:13 That makes total sense. That's absolutely brilliant. The next thing I'd love to quiz you about is thinking about how types are constructed in your application and in your library, which is that I've spoken to a few different folks for this now.
18:31 A lot of what emerges is that you have these type helpers which build out the idea of what your library is supposed to be doing. They give you the terminology for doing that, too. I'd love to talk a little bit more about type helpers and what you feel the use cases are in application code, especially, and just your mental model for them. I'll share some code again. Let's dive in.
Gabriel: 18:55 Type helpers are basically utility functions, but for the type-level. It means that you dissolve a problem that occurs all the time, that's very generic, and they usually are pretty small.
19:19 If I scroll all the way up, when you see this notation, Partial is a type helper that's provided by TypeScript, and you call it by using those angle brackets. You pass a parameter to this function, it's going to return a type that's a bit different, but it's based on your parameter.
19:41 In this case, Partial returns an object, which has all the keys of your input object, but they are all optional. This is built into TypeScript, because this is such a common problem that we have.
19:55 Omit is another one that's very common. Let's say you have an object and you want to remove one of the keys, because it doesn't make sense for a part of your code. Well, if you use Omit, it's going to return the same object, but these keys will be erased basically.
20:14 You have slightly more complex type helpers, like Extract, which is essentially filtering a union type. In this example, we have some Actions, we have setAge, setName, getAge, and getName. We only want to keep the elements of this union that are assignable to this type over there. This type is the type of string that starts with, basically, anything and always ends with name.
20:47 Here, if we do that, we get back set name and get name only. That can make a lot of sense. Let's say you are building a form library, you want to be able to update some fields and you have an input, specifically, for you to use a name, you can filter the list of methods or actions these components can do to exactly what it's supposed to do.
21:14 That's the use case where you would want to use that. We have a lot of the small helpers available. Sometimes, in your application, you might see a recurring pattern, you are always transforming your types in the same way. That's where defining your own helpers becomes very valuable.
21:35 Let's say in my application, I have a front-end version of a user object. It doesn't have an ID because it's not persisted. We also have an API version of this user. This API version is the same type, but it has this extra ID property to add to it. TypeScript is not going to provide me with any tools to add this ID thing because it's fairly specific to my use case.
22:05 I can define it with just an intersection. I create a function. It takes any barometer. It's going to intersect that with this object that only contains the ID property, which is a string. In TypeScript, when you intersect two objects together, it gives you the object that has all the properties of the first object and all the properties of the second object.
22:30 That's how you add keys to an object. That's one example. There are many, many use cases where it's really useful to use the type helpers. Defining your own type helpers also help you think at the level of abstractions that suits you. When you use Lodash, for instance, you use utility functions all the time.
23:00 You start thinking in terms of these utility functions. If you want to transform an array, you start thinking that it's a map operation. You are going to use the map function from Lodash. Then maybe you are going to turn that into an object. Then you want to map over the values of this object.
23:18 You start thinking in this higher level of abstraction, which is going to be more productive for you because you need to spend less time thinking about your problems, because all of the subproblems have already been solved, and you know their solutions. Type helpers let you do that at the type-level.
Interviewer: 23:36 They let you think in terms of the Lego bricks that you need to construct the problem. That utility Lodash is such a great metaphor. Really, really nice. Then they also let you build your own Lego bricks to solve problems that you're seeing. Then that just adds to the pile.
Gabriel: 23:53 Exactly.
Interviewer: 23:54 Talking at a higher level, is this something that you tend to use in application code? Is this something that you would have a types file in your apps, something for this?
Gabriel: 24:05 I think it makes a lot of sense both in library code and in application code. Library being highly reusable code, highly generic code, that might be an open-source package, for instance, and application code is solving real business problems that you have.
24:25 In both cases, it's good to have type helpers, because you may have problems that are very common in your domain of application, but it's also not very common, generally. It only makes sense for your problems. I work at Datadog. We deal with data. We have different data. We have logs and metrics, for instance.
24:51 Those two things, we need to represent them, in a way. We need to find a way to represent them. We have a lot of helpers that anybody can use in the company that helps people deal with the complexity of those things. We never open-source that because it's just for our problem, our domain.
Interviewer: 25:14 You work with engineers at Datadog to figure out what they need and build abstractions to help them. Engineers are doing that all the time in those things.
Gabriel: 25:24 [inaudible] process. You see something, you see a recurring pattern. You start abstracting over it, and you create a function. Sometimes it sticks, sometimes not. Over time, you end up with a good library of utilities.
Interviewer: 25:37 That sounds like it works for both JavaScript and for TypeScript, right? You can do that in the value world, but also in the type world as well.
Gabriel: 25:46 Exactly.
Interviewer: 25:49 Let's go on to conditional types. You gave a really, really nice explanation for union types and indexing earlier. I was wondering if you could talk about conditional types because conditional types have a reputation for being hard to read, a little bit hard to understand. I'd love to know especially what you feel they're for in application code or the more complex end of application code.
Gabriel: 26:18 I actually love conditional types. I think it's one of the most exciting features of TypeScript. It's really what makes the type-system of TypeScript a real programming language. Just to give a brief explanation, conditional types is just code branching, but for types. They always start with this notation where you have sum type A extends sum type B. Then you have two branches.
26:46 [indecipherable] branch, this is what is going to be executed if this condition passes, and a [inaudible] branch, which is going to be called if this condition doesn't pass. It really looks like a ternary. It's similar to a ternary. It's also different. That's where the confusion comes from. The interesting part is really this condition part A extends B.
27:17 A extends B actually means...it's a question we ask the compiler. We ask, "Is the type A assignable to the type B?" This is also a counterintuitive part of TypeScript in some ways. One good way to think about it is to try to picture -- if you had a value of type A in your code, a variable, and you add a variable type B, would you be able to assign the variable type A to the variable type B?
27:53 For instance, if you have variable type, let's say, two, and a variable type number here, and the answer would be yes, right? Because you could assign a variable of type two to a variable type number, because number includes the type two. This is where this idea of types being sets of values emerges.
28:23 Types essentially represents a bunch of values, so the number types represents all possible numbers, but the type literal two represents only one single number, which is number two. We hardly use literal types to contain a single thing. We usually use them in union types.
28:48 It makes more sense with strings, for instance, but you can go left or you can go right. I'm going to represent that as a union of strings, right? If you check if it's assignable to string, then it's going to be 1, because both left and right are assignable to string. Does that make sense?
29:15 We can see that it's a directional relationship. If we invert those two types and check if string extends left and right, it's going to be because some strings in the string set are different from left and right, most of them actually. This condition is not going to pass. That's how I tend to think about it.
29:45 Two reasons for why it's a bit confusing. First, this extends keyword. You may have noticed that I'm never using extend. I'm not saying string extends. In terms of assignability, I'm thinking is string assignable to...?
30:01 Just the choice of this keyword makes this feature a little bit hard to read and hard to understand, because what does extends mean in that context exactly? Because we know extends from object-oriented programming, where it means we are assigning new properties to a class.
30:20 Here, it's really not that at all. Most of the time in TypeScript, extend is about assignability rather than actual extension. Even though there is a connection between those two ideas, but it's not necessarily super intuitive.
Interviewer: 30:42 I feel like we should make a VS Code extension where it just cuts out the extends in that part and replaces it with assignable to or something similar. That would be really nice.
30:55 This idea then of the kind of union type multiverse comes into play here, because if we imagine we have left or right, and we say, "OK, we know it can extend string," what happens if I say left or right assignable to left or is assignable to left? How does this make sense in that mental model? What happens if I say left or top, for instance? What happens there?
Gabriel: 31:22 Let me refactor this code a little bit. I'm going to create a generic function instead of an inline conditional type. There is a good reason for that. Let's say I have this function that takes some variable. I ask TypeScript whether this variable...Oh, I saved them everything, 13, 14, 15.
31:53 We have this function that takes this parameter T and then use T extends. It checks if whether T is assignable to this union left or top. If I call this, F, I'm going to get 1. Left is assignable to this set of two things, you can assign a variable that contains left to a variable that can contain left or top, right.
32:29 Now, if I try left or right, that seems a little bit confusing. I will get [inaudible] . Boolean, in TypeScript is an alias for 1 or , I guess, in most languages. This is really what we get. This is the result of those two branches. What is happening here?
32:57 Once again, it's about this idea that union types will be in different versions of reality, and that when you want to use a union type in an expression, you need to evaluate this expression for each member of this union individually and get back their results as a union type.
33:16 If I have either left or right, it means that sometimes it's going to be assignable. It's going to be 1. In some other universes, it's not going to be assignable. It's going to be . We evaluate all of these possibilities and we get back the union of the results.
33:32 This is rather counterintuitive, especially because if you map the mental model of ternaries to conditional types, and it works for the most part, you start feeling comfortable. Suddenly, you use a union type, and it's totally different.
33:55 We just need to also acknowledge the fact that some concepts exist at the type-level and don't exist at the value level. It's important to understand them and know them. Union types is a great example of that.
Interviewer: 34:11 The right mental model for this then is that function runs basically once on left and returns 1. If we were to like change this to a one, let's say, then we would get or one coming out. Left is assignable to this. That runs once, returns one, and then right goes on it. Then it's not assignable to this. It returns and let's say this is zero instead. You end up with both branches being filled in.
34:39 I'm tempted to go into the rules for like distributivity and why this works, why you refactored it to this shape and not the other shape, but I'll leave that for something else, because we've only got about 10 minutes left and I want to talk about something that I really want to get your take on, which is mapped types.
34:59 Why don't we dive into the final file that we've got here, and you can talk me through this?
Gabriel: 35:05 Mapped types is another feature of TypeScript that I really love. It's extremely powerful and it's a very short notation. Both of these features are pretty positive.
35:21 Since it's so tough, it can be also pretty confusing, because you can easily end up with code that is very complex and very short. This is hard to read for people that are not used to that. That's where understanding mapped types and being able to decompose those big pieces of code into smaller chunks in your head and recompose them once you understand all the parts separately is great.
35:53 Mapped types, to put it simply, are just a way to turn a union of strings or whatever into an object. This is exactly what we are doing here. We have a union of four letters, A, B, C, and D. Here, we are generating this test type. If I add the magic comments to display our type, you will see that it's going to generate this object that we see below.
36:29 How does that happen? The difference with creating an object by hand is that you're not going to list all of the properties yourself. Instead, you are going to use this in keyword.
36:43 In takes a union type on the right and variable on the left. What we are doing here is that we are looping through all elements in this union. For each of them, we are assigning them to these elements -- variable. This is going to be our key. It turns out we also use it as our value here.
37:09 That's why we get this object that has the same keys and the same values everywhere. Where does the name come from? Map types, it's mysterious. [inaudible] mapping exactly here. It comes from the fact that the most common use case for this feature is mapping of an object property.
37:34 If we scroll a little bit -- here, let me change that -- we have our user type that we have seen already. We want to compute a partial version of this type. We want all of the properties to be optional. We could have used the partial type helper. Let's re-implement it ourselves just to see how map types works.
38:05 The way we are going to do that is by first turning this object into a union of keys. Then mapping through or looping through all of those keys, one by one, and use this key, each key, to index this input object type and get the corresponding value, one by one.
38:30 The only thing we are changing here is adding this question mark after the key, which is going to turn our object in a partial object as we want it to be. Something that can be confusing to beginners that there is nothing special about the Keyof keywords here. In Keyof is not a thing in TypeScript.
38:53 In is a thing. It takes a union type. Keyof is a way to get union type problem. That means that if we replace this by our current union type of Keyof user, it's going to work as well. Get the same result.
Interviewer: 39:24 That's really nice. I like especially the idea that it's basically just, we have a value on the one side, and we have a union on the other side. Doing this, it gives you a really nice model for actually just mapping over members of a union, right? It can act like a for loop. Is that right?
Gabriel: 39:48 Yeah, that's right. We can use this pattern to update every member in a union type. Here I have an example of that. We have our union from the beginning. Let me erase it. We have our union of letter here. Let's say we want to turn that into a union of strings that all start with letter is, and then our current letter, we can do that with a map type.
40:21 The way we do it is by first turning our union into an object. For each letter, we have a corresponding value. Then we read all the letters at once using this trick that we have explored earlier of reading an object using a union type. This way we get back this union of strings, exactly what we wanted. Letter is A, letter is B, letter is C, letter is D, etc.
40:53 This is a very common pattern because this in keyword makes it pretty intuitive and pretty easy to look through union types like that.
41:03 In this particular example, it turns out there is a simpler solution, and I'd like to mention it. Since union types have distributive property, like they evaluate for all of their branches and return the union of all possibilities, we can use that to just interpolate the Union variable, which contains A, B, C, D, into a string. What we'll get back if we do that, the same as before.
41:35 You might be wondering, "Well, this is obviously shorter and better, so why are we using a mapped type for that?" There are use cases where this pattern is extremely useful. One of them that I really like is this type helper that I use very often, Entries. Entries is there to turn an object type into a union of key-value pairs.
42:08 For instance, if we try that with our user type from earlier, we will get back this union of tuples. The first element is always the key and the second is always the value. You can see that the mapping between the key and the value is preserved. Age is wrapped with number and name is wrapped with string.
42:34 Why is that useful? It's useful every time you want to make transformations to objects that are a little bit more complex than just picking a key, omitting a key, etc. For the sake of any use case to express or show that, let's say we want to filter out every keys in our user types that are assignable to a number. We can't use Omit because Omit it only takes the keys that you want to get rid of.
43:07 Let's say we don't exactly know how many keys there are in our user objects and we can't list them easily. What do we do? We can't really find those keys very easily, unless we first turn our object into a union of entries. Let's do that. One way I like to think about it, by the way, is to ask me the question, how would I solve this problem in JavaScript? This is often very useful.
43:40 In this case, in JavaScript, this is what I would be doing. I would first turn my object into a list of entries with Object.entries, which you probably know. Then I would filter those entries with some logic. Let's say here I'm checking that the value is a number with type of value equal, equal, equal number. Then I will reconstruct a new object using fromEntries by passing this filteredEntries value.
44:14 This is very nice. Why don't we do it at the type-level, too? We actually can. The way we would do that is by first calling Entries. It's the same process. Then we exclude any element of this union assignable to this type. Here, the type I'm using is this tuple. I'm using any as the first value because I don't really care about it. Any key can be assignable to it. Any does that.
44:45 Here, I'm only going to keep values that are assignable to number. If it's the type tuple two, then it's going to be filtered out because it's assignable to number. If it's a string, it's not going to be filtered out because it's not going to be assignable to this any and number tuple.
45:04 Once I've done that, I can just call from Entries, which is defined here, which is another mapped type. This is going to output exactly what I wanted. Just to show you that it works first, if I add the magic comment, you can see that I only get name back.
45:24 This fromEntries thing is pretty interesting, too, because it is another feature of mapped type that is very useful, which is called key remapping. This is using this as keyword. In addition to looping through a list of things, we are going to use a different type for the key than the member of the union type itself. That's really useful in this context because entries here is a union of arrays. It wouldn't be a valid key.
46:00 You can't use an array as the key for an object. It has to be a string or a number, or a symbol. We need to find another way. We also want to be able to access the full tuple on the right-hand side here to extract the value for this key.
46:19 That's what we do here with E, which is the element of this entry, a single entry. We could rename it. We decide to use the second element as our value. We use as to use the first element as our key.
Interviewer: 46:42 That's super nice. If you try to do, for instance, entry in entries zero, which would give you the right key, you wouldn't be able to access the member. This would, basically, be the key, not the entry there. By using as, you get the best of all worlds, right? You can say, "I want to have access to the entire entry, but I also want to remap that entry onto a key."
47:10 This actually blew my mind when I first saw it. I thought that this thing had to be assignable to property key. I didn't realize you could just stick anything in there. It's fantastic. Using as gets you out of a lot of holes like that.
Gabriel: 47:22 It's really nice. What's even better with this pattern is that we can actually abstract about that because everything by value can become one of our utility functions that's built on top of other utility functions. It's basically the same code, except that T is a parameter. It's not user. It's any object.
47:43 Condition is another parameter. This is the type that we are going to use to check if we want to keep an entry or not in our output object. Suddenly, we have this omit-by-value thing. That's much nicer. It's what we wanted from the beginning, but we didn't have it. We can reuse it in any context.
48:05 This demonstrates how you can layer utility functions like that to build more and more complex algorithms and have smarter and smarter type inference.
Interviewer: 48:16 The Lego bricks that you just stack on top of each other and you end up with the awesome Lego brick at the top which you can just stick in anywhere. That's fantastic. Thank you so much. That's so useful. I think we've run over a little bit.
48:30 Let's aim to finish there. Thank you so much for sharing all of that, doing all of that preparation, and bringing all of that stuff here. This was absolutely world-class. Thank you so much, Gabriel.
Gabriel: 48:41 Thank you for inviting me. It was a pleasure.
Interviewer: 48:42 No worries.