While using TypeScript, there's something that we need to do quite often: define the type of a variable as one of multiple possibilities. For instance, let's say that the status of a button can be "hidden", "enabled", or "disabled".
How do you do that with TypeScript? You might know that there at least 2 ways:
++pre>++code class="has-line-data" data-line-start="14" data-line-end="24">// With string enums
export enum ButtonStatus {
HIDDEN = 'HIDDEN',
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
};
// With union types of string literals
type ButtonStatus = 'HIDDEN' | 'ENABLED' | 'DISABLED';
++/code>++/pre>
There are also basic enums (without the = 'HIDDEN' part) but we won't cover these because the drawbacks are too obvious to make it a fair fight: for instance, when debugging you get an obscure number for the value instead of something you can use.
And there are probably a million other solutions, but let's compare the two outlined above.
As seen above, declaring a union type is terser.
To use it, a modern editor like VS code will give autocompletion in both cases. But enums need to be exported and imported, while you get union types completion immediately.
2 points for union types!
With union types:
++pre>++code class="has-line-data" data-line-start="44" data-line-end="46">type DynamicButtonStatus = ButtonStatus | 'LOADING';
++/code>++/pre>
And with enums? It's not really possible ??
1 point for union types!
++pre>++code class="has-line-data" data-line-start="54" data-line-end="70">// Union types
switch (key) {
case 'HIDDEN': return ...;
case 'ENABLED': return ...;
case 'DISABLED': return ...;
// Notice that there's no default case
}
// Enums
switch (key) {
case ButtonStatus.HIDDEN: return ...;
case ButtonStatus.ENABLED: return ...;
case ButtonStatus.DISABLED: return ...;
default: return ...;
}
++/code>++/pre>
With enums, the default case is compulsory, TypeScript does not detect that all cases are already covered. This is a bigger problem than it looks: If you change the enum to add a status, you might forget to update you switch statement.
On the other hand, with a union type, you will get a TypeError and be reminded to handle the new case.
2 points for union types!
This is not the most common thing to do, but we might want to iterate over our button statuses:
++pre>++code class="has-line-data" data-line-start="82" data-line-end="100">// Union types
// ? Can't iterate over a type, defining an array is needed
const ButtonStatus = ['HIDDEN', 'ENABLED', 'DISABLED'] as const;
// Notice that the array and type can have the same name if you want
type ButtonStatus = typeof buttonStatuses[number];
for(const status of buttonStatus) {
// ? the type of status here is ButtonStatus
// ? always iterate in order
}
// String enums
// ? Just do it
for(const status in ButtonStatus) {
// ? The type of status here is string
// ? iteration order not guaranteed with "in"
}
++/code>++/pre>
This is a tough one, let's give? 1 point to enums!
At some point, we might change our mind and say that we want our 'ENABLED' status to be called 'PRESSABLE' instead.
With enums, this is just a matter of right click > Rename symbol
With union types, you will have to fallback to Find and replace in whole project, which is a bit less precise?
2 points for enums!
1 bonus point for union types!
In our perfectly fair comparison, union types get 6 points and enums get 3 points!
This means using union types by default, and maybe making an exception if you like to rename things all the time or your tooling doesn't provide live typecheking and code completion.
But perhaps more importantly, keep in mind that this topic is a bit like tabs vs spaces: while it's true that there are arguments for both, which one you're using doesn't matter that much in the end.
My hope with this article is that you can understand the trade-offs, pick one of the two options for your project, stick with it, and never have to talk about it again :).