The headlining feature here has to be packed enums. You can think of these as regular enums, except that instances of them can also hold onto other data.
The problem with vanilla TS enums
The following is a standard TypeScript enum:
We can use it like this:
Pretty simple, right? But what if we wanted to store the intensity of that color as well? Well, we could maybe make a type:
Let’s also make a function that takes a Color and makes it into an [R, G, B] tuple.
And that looks fine at first, but what if our client comes to us and kindly informs us of the following:
MAKE IT SO I CAN USE RGB VALUES OR YOU’RE FIRED!
Well, ok, can’t be too hard. Let’s just add Hex to our enum…
And update our type:
Wait, what should our type be?
string? I guess it would work if I change all the code that’s written to handle numbers.
number | string? A bit nicer, but what if someone tries to set a number for a hex value?
class Color with conversion functions? Bloat much?
any? Yeah, no.
And even if you do all the changes to handle hex today, what are you going to do tomorrow when they ask you for hexadecimal and HSL and HSV and alpha channels and who knows what else?
Packed Enums, that’s how! Let’s work through the same example with one.
pack<Enum>(k, v) takes in an Enum and packs it into an object based on the key and value of that specific enum entry. This means you get autocomplete in your IDE for both key and value.
Not as clean as the default TS enum syntax, but note that we passed the intensity into pack(), which means we don’t have to define type Color like we did before.
Now let’s make that toRGB function again:
Great! We have all the functionality of the original again! Now let’s start implementing hex colors by updating our Color enum:
TypeScript will start screaming at us as soon as we do this:
This might look intimidating at first, but it’s TypeScript telling us that our match() call doesn’t handle the Rgb case we just added to our enum.
The same thing will happen if we try to pack an invalid value into Rgb:
The TypeScript compiler will tell us what we did wrong in three different ways:
Valid values obviously work:
This works, but the return type is now number[] | [number, number, number], which isn’t ideal. We can work around this with the as keyword:
Another option is to return Enum<Colors>, which is useful if you want to keep using the functions built to handle the enum:
Both are fine, do whichever makes more sense for your project.
And of course, fearlessly add new features, knowing the compiler will tell you exactly where your code needs to be updated to handle them:
(Almost) Zero-cost
This is how TypeScript compiles the ColorEnum we built earlier:
And this is how it compiles a crabrave enum:
And in fact, the entirety of the enum logic compiles down to just this:
Or, after minification: var o=(...i)=>i,w=(i,u)=>u[i[0]](i[1]),x=(i,u,d)=>(u[i[0]]||d)(i[1]);export{o as pack,x as matchPartial,w as match}; (116 bytes)
This is a cost you pay once across your whole entire project, and then you can make and use as many enums as you’d like without any additional penalty.
The magic is in the type system, and that’s what vanilla TS enums miss.