Forgot to mention in the video, but union types are great alternative to const enums. Want your code to disappear at runtime? Don't need an object representation? Just use a union of strings: type LogLevel = 'DEBUG' | 'WARNING' | 'ERROR';
Enums have worked great for me. Readability and robustness that they introduce outweigh the potential pitfalls IMO. In fact, I've never even run into problems discussed in the video myself
I did run into problems like having to filter out keys of an enum if it were a number, but only because I was abusing enums to register categories instead of representing the categories as data
@@austinmajeski9427 yeah it's both of those things. I did that in a proof of concept for myself tho, I usually don't allow myself to be that "creative" if I'm working for a client lol. And I eventually just converted them to objects, since it was data all along
Me too. Many times I had to change union type values and I would have to replace each occurrence in the entire project, while refaforing enums is extremely easy. I also like that TypeScript prevents you from using the enum values directly, as this would negate all benefits of abstracting their values in the first place over union types.
I agree. If you stick to assigning strings to the enum keys, you avoid all the issues Matt describes. And I actually like that enums force you to write out Enum.Key instead of allowing the use of plain strings (like in string union type) as this makes for much better code maintenance in the long run (if the value changes in the future, you just need to change it in one place). @Matt Pocock, I love your content, but here I really disagree with you. I was expecting a strong case against enums, but I don't see one.
I have to disagree. After watching the video i feel like i prefer Enums the way they are in Typescript, than what you proposed. Not that i have not messed it up once or twice before, but i think, as you said, i would really dislike allowing strings in places where i require an enum. It's the reason i use the enums in the first place. In most cases you are also not supposed to dereference enums, i would say.
Additionally the use case shown from 6:16 does work the same with enums if you assign the strings to the keys. Using enums instead of strings makes it also way easier to rename the keys or values.
@@Scuubie Yeah.... I found it pretty annoying that he made it seem like a label map is required at 7:19. Like, he already showed earlier in the video that you can use strings as values for the enums. And doing so eliminates every problem mentioned in the video. The only "problem" it doesn't solve is that it doesn't let you use strings instead of the enum, which I think is actually a good thing. I found the majority of the video to be a really bad take overall. Also, the thing with LogLevel and LogLevel2 at 3:50 is just ridiculous. If you end up in a situation where you have two different enums that represent similar states, it was either a mistake that could be refactored, or there's an important distinction, or each enum is from a different code library. In any case, if you find yourself in this situation and can't refactor your way out of it, "just using the string values" is like the worst choice you could make. Lol. Most likely, the right decision would be to have some function that converts one enum to the other. Even if you're using the "as const" thing, using the string values on their own just seems like a bad idea
Yeah I’d absolutely reject a PR that uses strings and “enums” interchangeably it’s not maintainable and reeks of the sort of thing you’d do in a small single developer codebase because you think you can remember all your cute tricks a year later.
The best thing about the content of this channel is that they're short, succinct but massively useful. Much better than watching a 60 minutes presentation
Honestly the fact that enums aren't just structural is an advantage to me. The main point of using enums is that you use enums and not strings. String enums have the best behaviour out of all the options mentioned.
2:25 I don't think this defeats the purpose of enums and in fact should be the recommended way of using enums since it mirrors the const object. I find you don't want to ever allow bare strings as values that as supposed to be mapped to a specific thing...makes refactoring much harder...and I also make it a rule not to look at compiled code and stick to the actual source. I use enums like this: enum LogLevelEnum { debug = 'DEBUG', warning = 'WARNING', error = 'ERROR', } type LogKeys = keyof typeof LogLevelEnum; type LogLevel = typeof LogLevelEnum[LogKeys]; let logLevel: LogLevel = LogLevelEnum.debug;
Yeah, the thing he said about it defeating the purpose of an enum was incredibly silly and nonsensical. Especially since his recommended method involves making a POJO with strings as the values anyway. Lol. It's basically the exact same thing, but the only difference is that his method requires a ton of boilerplate, and it allows people to use strings instead of the enum, which, as you also pointed out, is a really terrible idea. Lol. I really do not understand the point of this video other than to warn people not to declare bare enums that auto-define number values. That is a useful thing that I learned from this video. Everything else seemed like it wasn't thought through very well....
A lot of the things you call weird are safeguards. Passing some random string as typed enum argument should always be an error. Do you want someone to pass in 'debug' when there will be a comparison against 'DEBUG'? You say breaking the rules, I say following the rules. Literals are almost always a code smell in my book. An ability to exchange enums is asking for trouble. @6:47 you've just rewritten enums that use '=' syntax as a const and made the string formatting more verbose.
But the values of the string literals are still type checked. You can't pass any string literal that isnt allowed. So if you are only allowing 'DEBUG' and you pass 'debug' it will be an error
@@JonathanRose24 Here's a small demo of enum being "stronger" than literal function litfun(flag: LitFlag) { .... } function efun(flag: EnumFlag) {...} if both have "debug" in their type, you can pass EnumFlag's "debug" to litfun, but you can't pass literal "debug" to efun. Example is contrived, but the point is to show that sometime you want an explicit type, not just a string that happens to match.
@@NuncNuncNuncNunc ok but how’s this a benefit? To me this is a negative as pointed out in the video because it works differently than how types work for everything else, that being structural typing. I guess where we differ is that I see no value in this explicit type.
@@JonathanRose24 Then it is a matter of preference. We're stuck with typescript comparing shape instead of type. My preference is for type checking. With respect typing, some use branded/tagged types to get better type safety. NgRx Actions feel like a proto version of this pattern. It's a pain having to do runtime type checks, but it's the best the language allows. PS I use both literals and enums where I feel appropriate, so this is not a dogmatic position.
Enums made me waste a few days trying to figure out why my tree shaking was not working. It was cause enums. When the bundler goes through, it sees the self invoking function as a side effect and includes it no matter what.
If you'd like to allow the string representation of the enum you can do something similar enum LogLevel { DEBUG = 'DEBUG', WARNING = 'WARNING', ERROR = 'ERROR', } function log(message: string, level: LogLevel | `${LogLevel}`) { } log('Hello!', LogLevel.DEBUG); // Works log('Hello!', 'DEBUG'); // Also works
Yep. This makes enums even more pointless than they already are. One could argue that invariance is good-in that two different enum types with same value should not be considered the same-although I myself disagree this is a good thing, because "DEBUG" is "DEBUG" is "DEBUG". Always. But in your example the enum is now just overhead (emitted code + leaky abstraction). I could see this as being a good way to introduce temporary types in order to refactor _away_ from enums, so to stay compatible with existing code while also forwards compatible with unions. Good thing!
@@williamdrum9899 You can with const enum, but those are even more terrible for consumers that use JavaScript, because they have the same constraints as enum but aren't accessible by JavaScript.
This defeats the point of enums. Enums are *not* strings, they represent states. The states they represent can be “backed by” strings, but a string “DEBUG” is NOT the same as an enum with a key called DEBUG.
Thanks Matt, I shared this video with multiple people already, I knew enums are problematic but didn't really know why, you did great job making me understand. The way you plugged your payed course is perfectly done and I hope other from industry are taking notes.
i have different opinions on TS enums: 1- i think (like you said) the enums are the best option for code refactoring, and a great solution for keeping values throughout your code base the same. (when you change the values in one place, they change in every place the enum is used, if you just pass arguments from an enum, but as just strings, you will need to change every place that value is used); 2- about the thing that you shouldn't be explicit when using enums, because it sort of defeats it's purpose, i 100% agree with that, but i also think being explicit is way better than implicit, because someone may change the order of variables (in a conflict from merging, for example) and change it's values, or is just too inexperienced and mess something up; 3- (and this is about everything in transpiled JS) about the way it is done in javascript when transpiled, i think if it performs great, do it's job without problems and will not be a problem for the developers and users in the future, it shouldn't be something you care that much about, because you will not see this code, what you see/code is TS, the JS is just runned by the engine.
3:13 the point of an enum is *not* to represent a value, but a state. That’s why it’s not as straightforward as just passing the string “DEBUG”, because enums are not strings. Their values can be *backed by* strings, but their values are predefined and not variable. Enums act like their own types, and an arbitrary string can never be the same type as an enum whose value is backed by a string.
this is what can't get into my head still, that i should treat enums like it's not a value assignment, it's already its own value. Arghhh, the very first thing i learned in javascript is that everything is an assignment, now this enums thing became hard to understand
Your representation of how to achieve the same result as an enum at 7:11 was a little uncharitable imho, as you could have simply written: ``` enum LogLevel { DEBUG = 'Debug', WARNING = 'Warning', ERROR = 'Error', } function log(message: string, level: LogLevel) { console.log(`${level}: ${message}`); } log('Hey', LogLevel.DEBUG) ``` Which is almost identical to using a constant object literal. I've never had an issue with enums when the values are strings
5:44 this looks wrong, passing the enum value feels more static and less "magic" string, also if you've never saw it you have to stop and try to understand the two lines of code above it, seems an overkill for a non existent problem just for the sake of feeling clever.
Gonna make a few dot points to try understand what youre saying - 1. Enums, when compiled, map in both direction. Meaning if you are treating it like an object, you will have extra key: value pairs that you need to expect. 2. Typescript cares about the Names of Enums. You cant use the Values of the enums in place of the name of the enum. 3. A neat alternative can be either Union Types or Const Object 4. Const Object together with ObjectValues type has the benefit of being able to use the Name or value of the item. Benefits of Enums naming without the above two issues. 5. Another Benefit is having the Object have a machine readable key, with a human readable representation of the key as the value. Thanks for the tips! :)
BE AWARE: 1. Don't treat them as Objects, treat thems as Enums, maybe read how enums works in other languages to grasp the concept, maybe Java or C# or python, if you need to treat them as object, maybe are not enums or you need an extra map to define what you actually need. 2. if my data object has a propery call state, and its value is @ to define that is 'pending' (coming from backend) I can create a enum PENDING='@' and then check for obj.state === MyEnum.PENDING, if backend changes this to a string 'pending', I change the value of the enum and my code continue working. The concept of Pending still there, my code still readable, and the implementation of how pending is is protected by the enum. 3. This create problems when you try to create a list of values in a whitelist, you MUST typed the array that will contain those values, as ObjectValues[], using enums you don't need to because TS can infer it. The same happen every time you need to use that ConstEnum you need to specify that you require the values, at the end you endup exporting a MyContEnumValues then you are using 2 types for 1 enum, values and the map access to those values... is messy 4. Value of the enum shouldn't be used, if you need to use the Value then your abstraction is wrong somewhere, that value is representing somethign that you are not naming or/and your code is not specifing as necessary. 5. This is a given in enums enum States { Pending='%wait%' // Human readable = machine value, which you as developer shound't care how is even called. }
I don't agree with the general take of this video. If you use enums with string values, such as enum LogLevel { DEBUG: 'Debug', WARN: 'Warn', ... }, you have best of both worlds: strictness and predictability. I think the "as const" approach defeats the purpose, because you shouldn't want to pass the value of the enum, because you shouldn't be interested in the value, but only what it represents, i.e. LogLevel.DEBUG makes sense, but whether its value is 'DEBUG', 'debug', 'Debug', or 'dEbUg', I really shouldn't care.
3:13 Apologies for saying this but to me this is totally not weird. This is the EXACT reason why you use enum. You can't use some random string that happens to be "DEBUG" to equate to LogLevel.DEBUG, that's the whole point of using enum. I don't want someone passes, for example, ReportLevel.DEBUG to my logging function when what I really needed is a LogLevel enum, if there is a code that someone passes a wrong type of variable into a function, like this one, I want my compiler to tell me. I think If you've used C/C++ this behavior should be very natural to you.
Actually you don't need different names for const asserted enums and their type. You can have "const LogLevel = { ... } as const;" and "type LogLevel = ObjectValues;" in the same file. That's basically how @prisma/client generates enums from the schema.
Unfortunately, this is not perfect. The problem arises if you want to use "LogLevel.Debug" as a type, but are already importing both the type and the object. With enums it would be "type DebugMessage = { level: LogLevel.Debug; message: string; }". With as const typescript won't let you, so you'll need to write "type DebugMessage = { level: typeof LogLevel.Debug; message: string; }".
@@Вася-о3ф7ь Good point. You can "import type" and rename the import in such rare case where you import both and need to distinguish between type and const.
Wondering if there's an array alternative where you can get intellisense on like with enums (without values) const fieldTypes = ['email', 'number', 'address'] as const you now don't get intellisense for checks like "if type === fieldTypes.em..." - which you would get when doing it with an enum
Hard disagree. Using enums with manually assigned string values is the essence of what an enum is. It's just a complete list (an enumerated list.) Mapping values is a separate and entirely different toolkit in programming and it is convenient that it can be applied to TS enums. What you basically have uncovered is that "mapping" is a swiss army knife and "enums" is a hammer on the tool belt of programming.
You are right. Enum is just a tool to make the code base more organized, give a quick summary of all possible values for a particular property. That's all. Why does he making it such complicated, dont understand.
6:34 the little problem with this example, in my opinion, is if you didn't write a log function and just want to use it, you have no idea that you can use LOG_LEVEL object keys as an argument. You see a type LogLevel, maybe you tried to open a quote and realized that you can use some string values. But you should go to LogLevel declaration to see that this type is keys of object, so you can use this object. But I use this approach too.
The first example is actually exactly how I expect Enums to work, there is the ordinal value of the Enum and then there is the value you assign for the Enum itself.
3rd time in a week revisiting this video. I turned the two type definitions into a one liner. Not sure if there is an issue with it but at first glance it seems to work fine for me. " type LogLevel = typeof SIZE[keyof typeof SIZE];"
What about the case when enum value is used as a type: export type State = | { name: AuthState.Loading } | { name: AuthState.Error; message: string } Using string literal in `name` doesn't let me keep a single source of truth, but the alternative with `as const` seems to be `typeof AUTH_STATE.LOADING` which seems pretty convoluted. Could also extract the type, but it's also not pretty: type AuthStates = typeof AUTH_STATE export type AuthState = | { name: AuthStates['LOADING'] } | { name: AuthStates['ERROR'], message: string }
Enums all the way in big scale projects. You don't want to refactor hardcoded strings. Expression like log('Hey', 'DEBUG') is more harmful than log('Hey', LogLevel.DEBUG). (Unless you don't care about the future of your project)
In other languages I'd agree, however in TypeScript, you don't have to use "string" type parameters as an alternative; you can explicitly define a constant set of string values as a unique type, and pass in that type as a function parameter instead. type LogLevel = 'DEBUG' | 'WARNING' | 'ERROR'; function log(message: string, level: LogLevel) { } log('Hello', 'DEBUG'); // OK. log('Hello', 'ABC'); // Immediate compile-time error. This still has all the compile-time & refactoring benefits as enums. In VSCode, you can refactor/rename any value of the type and have it be updated everywhere its used like usual. As you're inputting parameters for functions, Intellisense will suggest only the valid values that can be used like usual. If you input anything that's not one of the valid defined values for the type, you get a compile-time type-matching error like usual.
@@Dxpress_ I just tried it and it didn't replace some occurrences in React component prop. Moreover, when I search "find all references" by given literal type, it finds strings that doesn't belong to type being searched. Enums are much safer for refactoring.
I think your explanation at 6:00 missing an important aspect of using an enum, the ability to rename the symbol would be reflected in your entire application and you lose that benefit by just providing a string value to that log function. You would have to manually rename that.
I feel like this could be solved by disabling enumerability on the reverse map keys of the enum. At that point, you could still reverse lookup, but you could also still iterate through the keys.
Here's me again coming to one of your videos with a PR in with the exact thing you tell not to do. Thanks for your content! And definitely important to ask for the like as well. I would never remember to do it otherwise.
I can see the value in some of the solutions outlined in this video, but the one thing I find most useful about using enums is when working in a larger dev departments. Especially if some of the code may at some point be touched or viewed by members of other teams. It keeps uniformity in how you pass certain values, easy to check the available options with minimal code, and makes it easy to look up where it is used within a project. I like the as const approach, but I can't say that the arguments here against enums at least for most of my use cases justify a change in convention within a stable, well performing and large codebase. Nevertheless, the information is great to know and offers fantastic insights into the inner workings of TS!
Great vid. I have to acknowledge. Javascript may be one of those lands that are a bit odd in some implementations., but it is one of the most satisfying languages to learn. I still enjoy how much I can easily do with it.
ok … i’ve unknowingly run into this problem before, and also came to the conclusion that enums can product slightly less readable code (the double access). only occurred once or twice.
I think saying "don't use enums" is incorrect. I use them, and all of the "weird" things you mentioned I make use of. But I would agree that you shouldn't use enums if you don't fully understand how they work.
If I could give you 2 thumbs up, I would! Very cool, and I literally spent a day searching this information! So, lucky me :) PS It's a pity I won't be able to buy your course
Great idea! Just tried this, but I ran into an issue with react. `const [option, setOption] = useState(LogLevel.DEBUG);` The inferred type of `option` is "Debug" and not of LogLevel meaning it won't let me change the the value of option without explicitly giving useState(LogLevel.DEBUG); Which is the type of repetition that we were trying to avoid in the first place. I'll stick with enums for now.
Beware - try calling setOption with an arbitrary number. If you're using an enum it doesn't yell at you. useState (backed by as const) is more verbose but it's much safer.
Great explanation and nice video! I will still be using them though 😄 Working in a team the solution with "as const" and "typeof keyof" just seems a little too confusing. Plus I do not see the described flaws ever matter in a project where enums are used to define some values that would otherwise be hardcoded somewhere in the project
I feel like if Enums add something to the JavaScript side from the TypeScript side and I never have been happy about it… I realized that without thinking about it just because I was having all my types and interfaces in one same folder and Enum kind of needed to be in that folder and outside of it at a same time
I agree with the ”as const” alternative, but It’s hard to understand why would you want to enumerate a string yet still use magic strings everywhere. The point of an enumeration is to work as a constant abstraction for a parameter.
I see the key value repeating, we can do this, right? `const TYPE_LOG_LEVEL = ['Debug', 'Warning', 'Error'] as const; export type TLogLevel = (typeof TYPE_LOG_LEVEL)[number];` The [number] index is used to extract the type of the elements within the TYPE_LOG_LEVEL array. In this case, the elements of the array are string literals, so the type of the elements is string. Without the [number] index, the type of TLogLevel would be (string | number)[], which is an array of either strings or numbers. By using the index [number], we extract the type of the elements within the array, so the type of TLogLevel becomes string, which is what we want. The [number] index is used to extract the type of the elements within the TYPE_LOG_LEVEL array and make the type of TLogLevel a union of string literals instead of an array.
It's regrettable how awful TS enums are, not only for the quirks covered in this video, but also because of the missed opportunity to be an algebraic data type. Over in Rust land enums are quite powerful, not only in their ability to express variants of many types, but also in the way in which they can be comprehensively matched over and in how serialization/deserialization behavior can be derived and annotated as to also support things like tagged types. Error and Result enums are both incredibly useful
As a solo programmer yes, as const is the way to go. As an enterprise developer on a team of many teams, no import that enum cause trying to explain all this to a new dev that will get replaced next year is way too hard
i'm new to typescript and since i'm Java, enum doesn't look weird to me. I only respond to Matt question where he used string literal to pass as an argument to function, he said this looks more natural to javascript. What i understand this argument only makes sense if the argument we're passing is coming as variable that we need to pass as an argument. b/c otherwise if we're typing it doesn't matter if we type 'DEBUG' or LogLevel.DEBUG. So in case if it's coming as variable we can simply use as let logValue = 'DEBUG' printLogLevel(logValue as LOG_LEVEL) since i'm new to typescript, do let me know what you guys think.
It seems that most people here do not read books for beginners, and therefore do not understand what this thing is and what enum is used for. Don't blame the microscope for not being good at hammering the nails. Read about enum flags, clean code, incapsulation and typescript implementation (what's going on behind the curtains)
I think there is a distinction that needs to be made for using explicitly assigned enum values. The default behavior of enums, and the 'as const' behavior is a little odd, but using them as a glorified string map forces users of the code to be explicit about where the values they are using are coming from, rather than passing a string literal into a function.
I find myself using enums quite a lot. The main reason I use them over something like union types is that our codebase isn't 100% in typescript - we are still using Vue 2 and we don't use typescript in the components - so using enums helps me to avoid typos.
You don't use an IDE that auto completes? Use it! Unions would also tell you when you made a typo anyway. Use the TS type checker in your JS files (add @ts-check comment at the top of your files), and you will avoid a ton of problems from typos. Typos shouldn't be the factor that introduces bugs in your code. This is one of the reasons to use TypeScript in the first place.
Enums have worked great for me. The readability and robustness that they introduce outweigh the potential pitfalls IMO. In fact, I've never even run into problems discussed in the video myself
i wonder how these "as const" alternatives work in serialization. and if they're supported by major API frameworks (like swagger for example) guess i'll test it out...
6:15 Mixing up typing and formatting together is not "really killer feature", it's a breaking of SRP. Why at all I should bother with formatting in type related staff, what if I'll need more advanced formatting, etc ?
Forgot to mention in the video, but union types are great alternative to const enums. Want your code to disappear at runtime? Don't need an object representation? Just use a union of strings:
type LogLevel = 'DEBUG' | 'WARNING' | 'ERROR';
const LogLevels = ["DEBUG", "INFO", "WARNING", "ERROR"] as const;
type LogLevel = typeof LogLevels[number]
@@sachahjkl Yes, also a great case. Although LogLevels.includes is annoying.
Was about to point this out haha
Does there exist a way to have ESLint or similar give an error if the code contains enums?
Oh ya, I use this most of the time. I was wondering do we really need enums? Now I know why I thought that 🙂
Enums have worked great for me. Readability and robustness that they introduce outweigh the potential pitfalls IMO. In fact, I've never even run into problems discussed in the video myself
I did run into problems like having to filter out keys of an enum if it were a number, but only because I was abusing enums to register categories instead of representing the categories as data
@@austinmajeski9427 yeah it's both of those things. I did that in a proof of concept for myself tho, I usually don't allow myself to be that "creative" if I'm working for a client lol. And I eventually just converted them to objects, since it was data all along
Me too. Many times I had to change union type values and I would have to replace each occurrence in the entire project, while refaforing enums is extremely easy. I also like that TypeScript prevents you from using the enum values directly, as this would negate all benefits of abstracting their values in the first place over union types.
I agree. If you stick to assigning strings to the enum keys, you avoid all the issues Matt describes. And I actually like that enums force you to write out Enum.Key instead of allowing the use of plain strings (like in string union type) as this makes for much better code maintenance in the long run (if the value changes in the future, you just need to change it in one place).
@Matt Pocock, I love your content, but here I really disagree with you. I was expecting a strong case against enums, but I don't see one.
I've also been nothing but happy with Enums. The refactor-ability of Enums make a world of difference
I have to disagree.
After watching the video i feel like i prefer Enums the way they are in Typescript, than what you proposed.
Not that i have not messed it up once or twice before, but i think, as you said, i would really dislike allowing strings in places where i require an enum.
It's the reason i use the enums in the first place.
In most cases you are also not supposed to dereference enums, i would say.
Additionally the use case shown from 6:16 does work the same with enums if you assign the strings to the keys. Using enums instead of strings makes it also way easier to rename the keys or values.
Also using zero based unions of numbers prone to bugs due to being falsy.
@@Scuubie Yeah.... I found it pretty annoying that he made it seem like a label map is required at 7:19. Like, he already showed earlier in the video that you can use strings as values for the enums. And doing so eliminates every problem mentioned in the video. The only "problem" it doesn't solve is that it doesn't let you use strings instead of the enum, which I think is actually a good thing. I found the majority of the video to be a really bad take overall.
Also, the thing with LogLevel and LogLevel2 at 3:50 is just ridiculous. If you end up in a situation where you have two different enums that represent similar states, it was either a mistake that could be refactored, or there's an important distinction, or each enum is from a different code library. In any case, if you find yourself in this situation and can't refactor your way out of it, "just using the string values" is like the worst choice you could make. Lol. Most likely, the right decision would be to have some function that converts one enum to the other. Even if you're using the "as const" thing, using the string values on their own just seems like a bad idea
@@iamstickfigure and if you wanna use strings in your code, just use a union type.
Yeah I’d absolutely reject a PR that uses strings and “enums” interchangeably it’s not maintainable and reeks of the sort of thing you’d do in a small single developer codebase because you think you can remember all your cute tricks a year later.
Hey Matt, I like how you express yourself. Well-paced and with plenty of depth without getting long-winded. Awesome!
The best thing about the content of this channel is that they're short, succinct but massively useful. Much better than watching a 60 minutes presentation
Thanks pal! I thought I rambled a bit in this one by my standards.
+1 they are compact filled with knowledge. It’s probably the best format for a TH-cam video ❤
Honestly the fact that enums aren't just structural is an advantage to me. The main point of using enums is that you use enums and not strings. String enums have the best behaviour out of all the options mentioned.
2:25 I don't think this defeats the purpose of enums and in fact should be the recommended way of using enums since it mirrors the const object. I find you don't want to ever allow bare strings as values that as supposed to be mapped to a specific thing...makes refactoring much harder...and I also make it a rule not to look at compiled code and stick to the actual source.
I use enums like this:
enum LogLevelEnum {
debug = 'DEBUG',
warning = 'WARNING',
error = 'ERROR',
}
type LogKeys = keyof typeof LogLevelEnum;
type LogLevel = typeof LogLevelEnum[LogKeys];
let logLevel: LogLevel = LogLevelEnum.debug;
Yeah, the thing he said about it defeating the purpose of an enum was incredibly silly and nonsensical. Especially since his recommended method involves making a POJO with strings as the values anyway. Lol. It's basically the exact same thing, but the only difference is that his method requires a ton of boilerplate, and it allows people to use strings instead of the enum, which, as you also pointed out, is a really terrible idea. Lol.
I really do not understand the point of this video other than to warn people not to declare bare enums that auto-define number values. That is a useful thing that I learned from this video. Everything else seemed like it wasn't thought through very well....
did you use enums in a proper language like c#? He is right, it is not how an enum should behave.
you can just type.
enum LogLevelEnum {
debug = 'DEBUG',
warning = 'WARNING',
error = 'ERROR',
}
let logLevel: LogLevelEnum = LogLevelEnum.debug
commenting to support the channel. loving the content lately, hopefully more to come!
I am using enums for my whole framework. They are perfectly fine
A lot of the things you call weird are safeguards. Passing some random string as typed enum argument should always be an error. Do you want someone to pass in 'debug' when there will be a comparison against 'DEBUG'? You say breaking the rules, I say following the rules. Literals are almost always a code smell in my book. An ability to exchange enums is asking for trouble.
@6:47 you've just rewritten enums that use '=' syntax as a const and made the string formatting more verbose.
right, what you said i have the similar thoughts while listening to the video.
But the values of the string literals are still type checked. You can't pass any string literal that isnt allowed. So if you are only allowing 'DEBUG' and you pass 'debug' it will be an error
@@JonathanRose24 Here's a small demo of enum being "stronger" than literal
function litfun(flag: LitFlag) { .... }
function efun(flag: EnumFlag) {...}
if both have "debug" in their type, you can pass EnumFlag's "debug" to litfun, but you can't pass literal "debug" to efun.
Example is contrived, but the point is to show that sometime you want an explicit type, not just a string that happens to match.
@@NuncNuncNuncNunc ok but how’s this a benefit? To me this is a negative as pointed out in the video because it works differently than how types work for everything else, that being structural typing.
I guess where we differ is that I see no value in this explicit type.
@@JonathanRose24 Then it is a matter of preference. We're stuck with typescript comparing shape instead of type. My preference is for type checking. With respect typing, some use branded/tagged types to get better type safety. NgRx Actions feel like a proto version of this pattern. It's a pain having to do runtime type checks, but it's the best the language allows.
PS I use both literals and enums where I feel appropriate, so this is not a dogmatic position.
Enums made me waste a few days trying to figure out why my tree shaking was not working. It was cause enums. When the bundler goes through, it sees the self invoking function as a side effect and includes it no matter what.
If you'd like to allow the string representation of the enum you can do something similar
enum LogLevel {
DEBUG = 'DEBUG',
WARNING = 'WARNING',
ERROR = 'ERROR',
}
function log(message: string, level: LogLevel | `${LogLevel}`) {
}
log('Hello!', LogLevel.DEBUG); // Works
log('Hello!', 'DEBUG'); // Also works
Yep. This makes enums even more pointless than they already are.
One could argue that invariance is good-in that two different enum types with same value should not be considered the same-although I myself disagree this is a good thing, because "DEBUG" is "DEBUG" is "DEBUG". Always. But in your example the enum is now just overhead (emitted code + leaky abstraction).
I could see this as being a good way to introduce temporary types in order to refactor _away_ from enums, so to stay compatible with existing code while also forwards compatible with unions. Good thing!
Why weren't enums just a nickname that the compiler replaces with the number at compile time?
@@williamdrum9899 You can with const enum, but those are even more terrible for consumers that use JavaScript, because they have the same constraints as enum but aren't accessible by JavaScript.
This defeats the point of enums. Enums are *not* strings, they represent states. The states they represent can be “backed by” strings, but a string “DEBUG” is NOT the same as an enum with a key called DEBUG.
Thanks Matt,
I shared this video with multiple people already, I knew enums are problematic but didn't really know why, you did great job making me understand.
The way you plugged your payed course is perfectly done and I hope other from industry are taking notes.
i have different opinions on TS enums:
1- i think (like you said) the enums are the best option for code refactoring, and a great solution for keeping values throughout your code base the same. (when you change the values in one place, they change in every place the enum is used, if you just pass arguments from an enum, but as just strings, you will need to change every place that value is used);
2- about the thing that you shouldn't be explicit when using enums, because it sort of defeats it's purpose, i 100% agree with that, but i also think being explicit is way better than implicit, because someone may change the order of variables (in a conflict from merging, for example) and change it's values, or is just too inexperienced and mess something up;
3- (and this is about everything in transpiled JS) about the way it is done in javascript when transpiled, i think if it performs great, do it's job without problems and will not be a problem for the developers and users in the future, it shouldn't be something you care that much about, because you will not see this code, what you see/code is TS, the JS is just runned by the engine.
#2 only matters if you ever do any operation the checks or stores/reads the numeric value of the enum explicitly.
ran*
3:13 the point of an enum is *not* to represent a value, but a state. That’s why it’s not as straightforward as just passing the string “DEBUG”, because enums are not strings. Their values can be *backed by* strings, but their values are predefined and not variable.
Enums act like their own types, and an arbitrary string can never be the same type as an enum whose value is backed by a string.
this is what can't get into my head still, that i should treat enums like it's not a value assignment, it's already its own value. Arghhh, the very first thing i learned in javascript is that everything is an assignment, now this enums thing became hard to understand
Your representation of how to achieve the same result as an enum at 7:11 was a little uncharitable imho, as you could have simply written:
```
enum LogLevel {
DEBUG = 'Debug',
WARNING = 'Warning',
ERROR = 'Error',
}
function log(message: string, level: LogLevel) {
console.log(`${level}: ${message}`);
}
log('Hey', LogLevel.DEBUG)
```
Which is almost identical to using a constant object literal. I've never had an issue with enums when the values are strings
without the re-enacting of 'hey maccarena' by the hands: One of the better instructional videos on ts i have seen.
My Typescript understanding has improved a lot since I started to watch your videos, Matt.
Thanks for that
5:44 this looks wrong, passing the enum value feels more static and less "magic" string, also if you've never saw it you have to stop and try to understand the two lines of code above it, seems an overkill for a non existent problem just for the sake of feeling clever.
Gonna make a few dot points to try understand what youre saying -
1. Enums, when compiled, map in both direction. Meaning if you are treating it like an object, you will have extra key: value pairs that you need to expect.
2. Typescript cares about the Names of Enums. You cant use the Values of the enums in place of the name of the enum.
3. A neat alternative can be either Union Types or Const Object
4. Const Object together with ObjectValues type has the benefit of being able to use the Name or value of the item. Benefits of Enums naming without the above two issues.
5. Another Benefit is having the Object have a machine readable key, with a human readable representation of the key as the value.
Thanks for the tips! :)
BE AWARE:
1. Don't treat them as Objects, treat thems as Enums, maybe read how enums works in other languages to grasp the concept, maybe Java or C# or python, if you need to treat them as object, maybe are not enums or you need an extra map to define what you actually need.
2. if my data object has a propery call state, and its value is @ to define that is 'pending' (coming from backend) I can create a enum PENDING='@' and then check for obj.state === MyEnum.PENDING, if backend changes this to a string 'pending', I change the value of the enum and my code continue working. The concept of Pending still there, my code still readable, and the implementation of how pending is is protected by the enum.
3. This create problems when you try to create a list of values in a whitelist, you MUST typed the array that will contain those values, as ObjectValues[], using enums you don't need to because TS can infer it. The same happen every time you need to use that ConstEnum you need to specify that you require the values, at the end you endup exporting a MyContEnumValues then you are using 2 types for 1 enum, values and the map access to those values... is messy
4. Value of the enum shouldn't be used, if you need to use the Value then your abstraction is wrong somewhere, that value is representing somethign that you are not naming or/and your code is not specifing as necessary.
5. This is a given in enums
enum States {
Pending='%wait%' // Human readable = machine value, which you as developer shound't care how is even called.
}
@@_danyg_ Good stuff, I like the outlook. Thanks for sharing
Using POJO is always my preferred way to go now. Great video!
I don't agree with the general take of this video.
If you use enums with string values, such as enum LogLevel { DEBUG: 'Debug', WARN: 'Warn', ... }, you have best of both worlds: strictness and predictability.
I think the "as const" approach defeats the purpose, because you shouldn't want to pass the value of the enum, because you shouldn't be interested in the value, but only what it represents, i.e. LogLevel.DEBUG makes sense, but whether its value is 'DEBUG', 'debug', 'Debug', or 'dEbUg', I really shouldn't care.
Great video!
3:13 Apologies for saying this but to me this is totally not weird. This is the EXACT reason why you use enum. You can't use some random string that happens to be "DEBUG" to equate to LogLevel.DEBUG, that's the whole point of using enum. I don't want someone passes, for example, ReportLevel.DEBUG to my logging function when what I really needed is a LogLevel enum, if there is a code that someone passes a wrong type of variable into a function, like this one, I want my compiler to tell me. I think If you've used C/C++ this behavior should be very natural to you.
Actually you don't need different names for const asserted enums and their type. You can have "const LogLevel = { ... } as const;" and "type LogLevel = ObjectValues;" in the same file. That's basically how @prisma/client generates enums from the schema.
Unfortunately, this is not perfect. The problem arises if you want to use "LogLevel.Debug" as a type, but are already importing both the type and the object. With enums it would be "type DebugMessage = { level: LogLevel.Debug; message: string; }". With as const typescript won't let you, so you'll need to write "type DebugMessage = { level: typeof LogLevel.Debug; message: string; }".
@@Вася-о3ф7ь Good point. You can "import type" and rename the import in such rare case where you import both and need to distinguish between type and const.
Love it
such an underrated channel - thank you so much!
Waiting for the next video "Don't use letters! They can be harmful"
You could avoid "as const" TS syntax and keep the typing by creating a function that returns the constant object
Wondering if there's an array alternative where you can get intellisense on like with enums (without values)
const fieldTypes = ['email', 'number', 'address'] as const
you now don't get intellisense for checks like "if type === fieldTypes.em..." - which you would get when doing it with an enum
Hard disagree. Using enums with manually assigned string values is the essence of what an enum is. It's just a complete list (an enumerated list.) Mapping values is a separate and entirely different toolkit in programming and it is convenient that it can be applied to TS enums. What you basically have uncovered is that "mapping" is a swiss army knife and "enums" is a hammer on the tool belt of programming.
You are right. Enum is just a tool to make the code base more organized, give a quick summary of all possible values for a particular property. That's all. Why does he making it such complicated, dont understand.
very rarely such quality content
Hey Matt. Can you please go back in time a post this video about a year ago before I dotted them all around my library?
6:34 the little problem with this example, in my opinion, is if you didn't write a log function and just want to use it, you have no idea that you can use LOG_LEVEL object keys as an argument. You see a type LogLevel, maybe you tried to open a quote and realized that you can use some string values. But you should go to LogLevel declaration to see that this type is keys of object, so you can use this object. But I use this approach too.
The first example is actually exactly how I expect Enums to work, there is the ordinal value of the Enum and then there is the value you assign for the Enum itself.
Usually when I transpile an Enum, it just gets removed in Javascript
8:53, don't need to exit fullscreen, you can scroll down in fullscreen mode :)
3rd time in a week revisiting this video. I turned the two type definitions into a one liner. Not sure if there is an issue with it but at first glance it seems to work fine for me. " type LogLevel = typeof SIZE[keyof typeof SIZE];"
I just want to know how to export it?, Cause if I export the type after the const is't working...
Thanks for explaining the IIFE joke to us. That totally made it much better... XD
Nicely explained.. I'm looking forward for more on typescript from you..and yeah I want you to have your mustache back as well..😉
What about the case when enum value is used as a type:
export type State =
| { name: AuthState.Loading }
| { name: AuthState.Error; message: string }
Using string literal in `name` doesn't let me keep a single source of truth, but the alternative with `as const` seems to be `typeof AUTH_STATE.LOADING` which seems pretty convoluted. Could also extract the type, but it's also not pretty:
type AuthStates = typeof AUTH_STATE
export type AuthState =
| { name: AuthStates['LOADING'] }
| { name: AuthStates['ERROR'], message: string }
Enums all the way in big scale projects. You don't want to refactor hardcoded strings.
Expression like log('Hey', 'DEBUG') is more harmful than log('Hey', LogLevel.DEBUG).
(Unless you don't care about the future of your project)
In other languages I'd agree, however in TypeScript, you don't have to use "string" type parameters as an alternative; you can explicitly define a constant set of string values as a unique type, and pass in that type as a function parameter instead.
type LogLevel = 'DEBUG' | 'WARNING' | 'ERROR';
function log(message: string, level: LogLevel) {
}
log('Hello', 'DEBUG'); // OK.
log('Hello', 'ABC'); // Immediate compile-time error.
This still has all the compile-time & refactoring benefits as enums. In VSCode, you can refactor/rename any value of the type and have it be updated everywhere its used like usual. As you're inputting parameters for functions, Intellisense will suggest only the valid values that can be used like usual. If you input anything that's not one of the valid defined values for the type, you get a compile-time type-matching error like usual.
@@Dxpress_ Sadly no, it doesn't replace all occurrences when you rename literal type.
@@coolemur976 Where does it not replace occurrences? I just tried it now and it seems to work fine.
@@Dxpress_ I just tried it and it didn't replace some occurrences in React component prop.
Moreover, when I search "find all references" by given literal type, it finds strings that doesn't belong to type being searched.
Enums are much safer for refactoring.
@@coolemur976 Aye, fair enough then.
I think your explanation at 6:00 missing an important aspect of using an enum, the ability to rename the symbol would be reflected in your entire application and you lose that benefit by just providing a string value to that log function. You would have to manually rename that.
I feel like this could be solved by disabling enumerability on the reverse map keys of the enum. At that point, you could still reverse lookup, but you could also still iterate through the keys.
what when I get backend status in number?
What’s the advantage of a POJO with “as const” over something like a union of strings?
Because you have it as a value, so you do things like Object.values etc.
What do you think about the upcoming typescript 5.0 enum unification?
Here's me again coming to one of your videos with a PR in with the exact thing you tell not to do.
Thanks for your content! And definitely important to ask for the like as well. I would never remember to do it otherwise.
how should i type enums that are like this
export enum DisableType {
TEST1 = 22,
TEST2 = 23,
}
I can see the value in some of the solutions outlined in this video, but the one thing I find most useful about using enums is when working in a larger dev departments. Especially if some of the code may at some point be touched or viewed by members of other teams. It keeps uniformity in how you pass certain values, easy to check the available options with minimal code, and makes it easy to look up where it is used within a project. I like the as const approach, but I can't say that the arguments here against enums at least for most of my use cases justify a change in convention within a stable, well performing and large codebase. Nevertheless, the information is great to know and offers fantastic insights into the inner workings of TS!
Great vid. I have to acknowledge. Javascript may be one of those lands that are a bit odd in some implementations., but it is one of the most satisfying languages to learn. I still enjoy how much I can easily do with it.
ok … i’ve unknowingly run into this problem before, and also came to the conclusion that enums can product slightly less readable code (the double access). only occurred once or twice.
3:15 it does work for regular numeric enums.
His british-english is american-english friendly, and he makes things clear, which is a first from a skinny smart guy, well done Sir.
Regarding that "refactor in VS Code" argument: You can now refactor types, type members when they are strings. That's like 🤯
Nice, your approach explaining things is really objective and I love the pacing of the whole video. Thank you!
@2:35 that joke deserves an Awad award... Named after TH-cam's world famous Ben Awad, of course.
I think saying "don't use enums" is incorrect. I use them, and all of the "weird" things you mentioned I make use of.
But I would agree that you shouldn't use enums if you don't fully understand how they work.
If I could give you 2 thumbs up, I would! Very cool, and I literally spent a day searching this information! So, lucky me :)
PS It's a pity I won't be able to buy your course
Do you think about fixing the enum problem in the td-reset library
It's not really something we can touch because ts-reset only looks at global typings, not runtime behaviour.
The dad joke at 2:38 made me immediately like and subscribe :)
I would just use a string literal for that, like, “debug” | “warn” etc
So what exactly is the advantage of obect as const over string enum?
i like the `keyof typeof` solution the most ❤ it's just so simple and versatile. Nice vod!
Great idea! Just tried this, but I ran into an issue with react. `const [option, setOption] = useState(LogLevel.DEBUG);`
The inferred type of `option` is "Debug" and not of LogLevel meaning it won't let me change the the value of option without explicitly giving useState(LogLevel.DEBUG); Which is the type of repetition that we were trying to avoid in the first place. I'll stick with enums for now.
Beware - try calling setOption with an arbitrary number. If you're using an enum it doesn't yell at you. useState (backed by as const) is more verbose but it's much safer.
Great explanation and nice video! I will still be using them though 😄 Working in a team the solution with "as const" and "typeof keyof" just seems a little too confusing. Plus I do not see the described flaws ever matter in a project where enums are used to define some values that would otherwise be hardcoded somewhere in the project
Didn‘t even need to remove the fullscreen to press 👍🏻. Thanks!
Professor, This what I was looking for. Thanks. I subscribed.
A trivial point and it may be my editor theme, but I like being able to distinguish between an enum, object and string by their color.
Hay Matt! how would you use a POJO keys as a zod schema enum??
z.enum(Object.values(POJO))
@@mattpocockuk It doesn't seems to work. It expects values to be `readonly [string, ...string[]]`
Any link to interview with Anders you've mentioned?
Amazing video Matt... Thanks...
I feel like if Enums add something to the JavaScript side from the TypeScript side and I never have been happy about it… I realized that without thinking about it just because I was having all my types and interfaces in one same folder and Enum kind of needed to be in that folder and outside of it at a same time
I agree with the ”as const” alternative, but It’s hard to understand why would you want to enumerate a string yet still use magic strings everywhere. The point of an enumeration is to work as a constant abstraction for a parameter.
I see the key value repeating, we can do this, right?
`const TYPE_LOG_LEVEL = ['Debug', 'Warning', 'Error'] as const;
export type TLogLevel = (typeof TYPE_LOG_LEVEL)[number];`
The [number] index is used to extract the type of the elements within the TYPE_LOG_LEVEL array. In this case, the elements of the array are string literals, so the type of the elements is string.
Without the [number] index, the type of TLogLevel would be (string | number)[], which is an array of either strings or numbers. By using the index [number], we extract the type of the elements within the array, so the type of TLogLevel becomes string, which is what we want.
The [number] index is used to extract the type of the elements within the TYPE_LOG_LEVEL array and make the type of TLogLevel a union of string literals instead of an array.
It's regrettable how awful TS enums are, not only for the quirks covered in this video, but also because of the missed opportunity to be an algebraic data type. Over in Rust land enums are quite powerful, not only in their ability to express variants of many types, but also in the way in which they can be comprehensively matched over and in how serialization/deserialization behavior can be derived and annotated as to also support things like tagged types. Error and Result enums are both incredibly useful
How would you reconcile an ADT concept with duck typing though?
enum Result { Ok(T); Error(E) } // how do you think this should be compiled to JS?
Don't blame TS for this, blame JS. It's difficult to build something good on top of an "embarrassing toy language". Rubbish in, Rubbish out.
@@scheimong there's nothing about JS that prevents TS from having good enums, TS just is weak sauce
@@cat-.- simply as a Tagged Union
Ok(1) = { tag: "Ok", val: 1}
Err("failure") = { tag: "Err", val: "failure"}
Enums are not awful. You just don't know what their use is for.
As a solo programmer yes, as const is the way to go. As an enterprise developer on a team of many teams, no import that enum cause trying to explain all this to a new dev that will get replaced next year is way too hard
Yikes. I feel sorry for you for the caliber of developer you apparently work with that can't grasp the basics of types. Why bother using TypeScript?
the question here is whether your ide could find you all the usages when you pass a string instead of an enum value
Where can I watch the interview you mention ate the beginning of the video?
I usually use const in upper case to create words used as words in C+. Works for me. In C#, I use enums.
So to put it into Assembly/C terms, typescript enums are an array of pointers to strings rather than an actual enum.
i'm new to typescript and since i'm Java, enum doesn't look weird to me. I only respond to Matt question where he used string literal to pass as an argument to function, he said this looks more natural to javascript. What i understand this argument only makes sense if the argument we're passing is coming as variable that we need to pass as an argument. b/c otherwise if we're typing it doesn't matter if we type 'DEBUG' or LogLevel.DEBUG. So in case if it's coming as variable we can simply use as
let logValue = 'DEBUG'
printLogLevel(logValue as LOG_LEVEL)
since i'm new to typescript, do let me know what you guys think.
in case if anyone `still` link to this video, please see changelogs of typescript v 5.0 and beyond
Of which there are zero changes impacting this take
Oh wow, that why I had such an initial pain with TS, that explains a lot 😮
IIFE 🤣. Great video again! And completely agree with you that enums add little value (but more size) to your codebase 🎉
It seems that most people here do not read books for beginners, and therefore do not understand what this thing is and what enum is used for. Don't blame the microscope for not being good at hammering the nails.
Read about enum flags, clean code, incapsulation and typescript implementation (what's going on behind the curtains)
Coming from a C++ background, and given the described potential security issues with const, I much prefer the default implementation (non-const enum)
Tried you example, but still cant pass a string to a function that takes that type as argument. weird.
Omg, "as const" really sounds as a killer :O Thank you, Matt!!!!!!!!!!!!
I was thinking on why a certain enum doesn't act well as a type for hours just today... const ... as const is a blessing 😅
Ok so this is only for typescript when I saw the title I was a bit worried but nothing to fear?
Correct, this is just about TS
@6:22 I came across something like this (forget where) and I have used systems like that and it is very very useful.
Enums in TypeScript behave exactly as I expected as a Java, Python, C#, and C++ software engineer.
don't understand the last example. why do you need the map if the enum itself is a map?
Can this be used to replace library code for enums?
I think there is a distinction that needs to be made for using explicitly assigned enum values. The default behavior of enums, and the 'as const' behavior is a little odd, but using them as a glorified string map forces users of the code to be explicit about where the values they are using are coming from, rather than passing a string literal into a function.
I find myself using enums quite a lot. The main reason I use them over something like union types is that our codebase isn't 100% in typescript - we are still using Vue 2 and we don't use typescript in the components - so using enums helps me to avoid typos.
You don't use an IDE that auto completes? Use it! Unions would also tell you when you made a typo anyway. Use the TS type checker in your JS files (add @ts-check comment at the top of your files), and you will avoid a ton of problems from typos.
Typos shouldn't be the factor that introduces bugs in your code. This is one of the reasons to use TypeScript in the first place.
@@dealloc Guess that it would need more work than that since when I add the comment all the properties from mixins error out.
Enums have worked great for me. The readability and robustness that they introduce outweigh the potential pitfalls IMO. In fact, I've never even run into problems discussed in the video myself
i wonder how these "as const" alternatives work in serialization.
and if they're supported by major API frameworks (like swagger for example)
guess i'll test it out...
6:15 Mixing up typing and formatting together is not "really killer feature", it's a breaking of SRP. Why at all I should bother with formatting in type related staff, what if I'll need more advanced formatting, etc ?