Transcript from the "Enums & Subtypes" Lesson
>> You probably shouldn't actually be impressed at this point, because we haven't done anything awesome yet. Enums just haven't gotten fantastic yet. So this next example is gonna get pretty exciting. Sorry, I've already gone over this point. This is me just saying, hey, you could do the same thing in TypeScript.
[00:00:16] So I haven't really done anything impressive. It just looks a little different. All right, so you might get pretty offended here, because I'm gonna say something mean about TypeScript. So I want everyone to breathe it out, it's gonna happen. So what I want you to do is I want you to create a custom type called Custom.
[00:00:31] So it's just gonna have two fields age, that's a number so you guys can get started now, just to kind of read this out and a name string. I want you to create a union type of number, string, and custom and it's going to be called Item. Then I want you to create a method called append that takes in a list of items and pushes, Hello Frontend Masters.
[00:00:54] Then I want you to create a list of Items doesn't matter if it's empty or not, and then pass it to append. So I'll do that as you do that. So let's go back over to our nice TypeScript file. I'm just going to delete everything. And so the first thing is let's create our custom type, right?
[00:01:08] It should have an age, number, and name string, fantastic. Okay, so we have our custom type. Now let's create our union type. So let's go down here and go type Item equals number or string or custom. I worked once not too long ago at Netflix. I actually had a Mongo database that I had to grab some real-time events out of and it had 150 different type events and not a single discriminator on it.
[00:01:35] So I had, this was just a real part of my life for union types in which I had to suss out property names to try to type narrow into actual events. It was really painful. If I only had Rust, I had to use TypeScript. I could have just used enums to solve all my problems, but I just went through the emotional ringer there.
[00:01:54] All right, so now let's create a function called append, and it's going to take in a list of items. And it's going to mutate that items with push.hello fem. Awesome, so now let's create a list of items and let's pass it to append. So I'm gonna go cost items.
[00:02:14] I'm gonna just define it right here so TypeScript knows and I'm gonna go append items. Everything's fine, right? We should be able to do this because items are number or string or custom. So I'll even do a little console.log (items) here. Yank that, put it right here. So we kind of have this little double thing going on right here.
[00:02:37] And I can go over to npx ts-node. And when I execute it, you can see it went from no items to having an item in it. Okay, pretty neat. Yeah, union types. All right, one more task. I want you to create a list of numbers and I want you to try to pass it to append.
[00:03:00] So, const numbers is gonna be a number array. And I'm gonna call append. Where's your god now, TypeScript? Why am I allowed to pass in an array of numbers into something that clearly takes an array of items and I'm pushing a string into it? Where's my complaints? I could take items right here.
[00:03:35] I could print out all of that. And look at that. My list of numbers has a string in it. What the heck? Isn't that emotionally bruising just a little bit? Come on, okay, I care about it. But apparently no one else cares about it. Okay, so I hate that.
[00:03:54] Let's see how Rust handles it. Okay, we're going to do the exact same thing. Let's create a struct called custom. Let's create a union type called Item. Let's create an append method that takes in a list of Items. Let's add the string Hello Fem to it. And then in our main function, let's create that list of Items and pass it to append.
[00:04:17] Okay, so we'll do it one at a time here. So I'm just gonna delete everything inside of here. Create a new main function. All right, beautiful. So first, let's create our struct custom. Struct custom, it's gonna have an age, usize. It would make no sense to have a negative number for age.
[00:04:34] You can't live in the future like that. And then let's do name, just capital S string. That makes sense, right? Okay, now we need to do the item, a union type, right? Well, now we're gonna use something beautiful. We're gonna use an enum to do this, what? All right, so we're gonna call it Item, man, I'm just so excited.
[00:04:54] All right, I hope everyone's ready. So let's first start with the number case. So I'm gonna call it number. I'm gonna do a stubborn, do one of these. I know, what is that, a function? And I'm gonna say, usize. Okay, I'm gonna do it again and I'm gonna do String.
[00:05:10] And inside of here, I'm gonna do String. And then I'm gonna go MyCustom, inside of here I'm gonna have custom, the data type we just defined right there. Look at those guys, right there. Okay, so can anyone guess what's happening right here? Our enums can have subtypes. And now it slowly should start expanding your brain making you think, holy cow, what's happening?
[00:05:46] We've just added type discrimination in a way in which there is no way to have a type discriminator. So this would be something that's equivalent to wrapping every single one of these items in type number, value number, type string, value string, type custom, value custom in TypeScript. And then doing a type discriminator on it to see if it's actually that type.
[00:06:10] Then you'd have to do run time guards of if type is equal to number, I know my value is now a number. And so we're doing that, but we're doing this at a language level, which is pretty fantastic. Underneath the hood, an enum is just a union type, and c is how you can think of it.
[00:06:25] So number is type zero and it has a usize for its data value. The next one is type string or one type string and then it has the string data structure for it. The third one which is probably value two has the custom data structure for it. And so an enum should be the size of whatever your largest data type is underneath the hood.
[00:06:46] You don't really need to know about the memory part, but that's what's happening underneath the hood. And so it's doing all that magic for you, if you've ever worked with C stuff. All right, so let's create that append function. So append is gonna need to take in items, which is gona be a mutable reference to a Vec of Item, right, a list of items.
[00:07:05] We need them mutated. So we have to have a mutable reference, it's very important. And I'm gonna keep on saying mutable like that. Makes it more fun. All right, so items.push. Now notice I cannot go like this, Hello Frontend Masters. Look what it says. It says, whoa, whoa, whoa, whoa, whoa, whoa, whoa, whoa, whoa.
[00:07:25] You can't just hand me a reference to a stir. You have to hand me an item, okay? So I'm gonna have to go like this and I'm gonna have to go Item::String and then I can pass in, hello Fem. Now of course, it's gonna do one more complaint because you're passing in a stir, not a string.
[00:07:46] I didn't want to do lifetimes. So this is me avoiding lifetimes. So at the end of your string you can call into. We'll actually get into into here shortly. Into is just a trait that says, hey, I want you to convert this thing into the right type. And don't worry, you know the type.
[00:07:59] It knows it's supposed to be a string. It's a stir, a stir can become a string or you could call something like to_string on it and a create a string. Because remember a string is something that exists on the heap so we need our own version, our own copy.
[00:08:12] So when I call to_string it creates its own copy of hello, Fem. So that means if I go like this let items which is gonna be a Vec of item equals veck bang, there we go. I can now call append with a mutable reference to items. Of course, it's going to complain, I can't borrow items as mutable cuz you never said it was mutable.
[00:08:35] So let's jump up here. I've now stated it's mutable. We can now pass it to append, it works. Now one thing I can't just go off and do is I can't just go off and say have a Vec of usize and call append, why not? I literally cannot express the same thing that TypeScript can.
[00:08:56] The reason being is that a discriminated union has been built for me. Whereas in TypeScript land, it's just like it could be one of these types. So we can't have the same level as expression, but this level of expression allows for lists to be different types, but all in one list together.
[00:09:12] And then gives you this safety. You could just literally never run into a situation where you don't have items inside of a list. Whereas in this other one, you could have numbers that accidentally have strings or items or whatever because of how TypeScript works versus how Rust works.
[00:09:26] Pretty cool, right? Okay, so now we're starting to get pretty cool. Come on, tell me this is where it's starting to get pretty cool, right? Well, this means you don't have to do any more of this type of x equals number. You don't have to do a property check to try to type narrow yourself down.
[00:09:41] You have match statements. You have pattern matching to match yourself into what you want it to be. Pretty nice, okay. So there's no more magic type checking. I like that a whole bunch. Again, I already mentioned this. We technically could have got around this in TypeScript land by creating a wrapper type, a box value, to be able to discriminate against the three.
[00:10:01] And so let's talk a little bit more about pattern matching. I just want to make sure I have this all in here because I don't remember how much exactly I covered it but right here. So if I create an item number with the value 5 in it, I can match on foo and handle each one of the different cases, pulling out the inner value or the inner type.
[00:10:21] So this is a usize. This is a string. This is a custom. Because I've pattern matched I'm grabbing out the inner value. You can think of it like a lift operator. I'm lifting out the value or the sub type. With match you can just have only the one you care about, and then ignore everything else.
[00:10:41] Underscore just means every other possible pattern, do this. It's kind of like a default case. You can also do pattern matching in the pattern matching. Yo dawg, I heard you like pattern matching. So I can do, hey, I'm gonna pattern match a custom. And then that inner type, I'm gonna pattern match the struct Custom and only grab the age out.
[00:11:01] So now I have reference to just age. It's pretty cool. So you can just keep on pattern matching as much as you need to do and then you get the most optimal highly compiler optimized code out that just refers to age. That's pretty good. You don't have to do all that checking in if statements, which is a runtime costs, or as this thing's probably just some sweet offset that's happening inside of a struct.
[00:11:22] Lastly, you can do this kind of thing where you do an if statement, even inside your pattern match. So I can pattern match custom if the customer's name is Ricky, I can go hey, Ricky, or if the customer's age is greater than 33, I can tell right away you like Nintendo 64.
[00:11:37] But if your age is less than 30, you probably love Xbox. And so I can do these kind of like if statements on addition to the match statement. And then in the end, it's gonna tell me I haven't covered every case yet. So I have to provide a default case.
[00:11:52] So it's pretty cool what you can do with pattern matching and then even what you can combine with it. It allows for pretty cool stuff. One really cool one is if you have a vector and you want to be able to grab the first and last element out of it, but it may be empty, it may have one item, it may have more than one item.
[00:12:08] You can actually pattern match first and last, and so only first and last that exist. You can grab those two out and the rest cases you don't wanna handle anything. There's like a lot of super cool. There's so many ways you can solve problems with pattern matching. I feel emotional about.
[00:12:22] I don't even know all the things. All right, do we have any questions? Speak up.
>> Yeah, one quick question. If you scroll up the pattern matching, does it always have to be an expression kind of written out or could you do like call function to match the pattern?
>> No, it has to be an expression that's written out. It has to be, it's a static thing because it's compiled at this point. So again, if you don't know anything about what's happening underneath the hood or you haven't used C style unions, when we create our little item, what's actually happening in memory is that there's the first part is the type discriminator, the second part is the data.
[00:12:59] And so 0 usize, I always put a z there. 1 is a string, which is a pointer, Length, capacity and then that thing, this pointer goes off onto the heap. And then two, you're gonna have custom. Which is gonna be age, which is a usize and then it's gonna have the name, which is gonna be the same thing pointer length capacity also off on here.
[00:13:25] And so when you do pattern matching, your item custom line is pretty much the same thing as saying, hey, when it's 2, go into this branch. So it's able to do that. So that's why you can't do a function, because it can't compile that thing in. It can't, it's dynamic.
[00:13:44] And so you see the same thing with switch statements, if I'm not mistaken, you can't have, at least in a lot of languages, you can't have a dynamic item within the switch statement. Because the switch statement is meant to do a table offset into it. So it's supposed to be faster than an if statement, a bunch of else ifs because it can just do a table offset, that's why it's supposed to happen.