Use Cases For TypeScript Discriminated Union Types and Generics
Enhancing Your Refactoring Skills with Practical Examples Using TypeScript’s Discriminated Union Types and Generics
What I love about TypeScript is its Type system. It is practical and feature-rich. Applying the types in the right use cases can make our code cleaner and easier to maintain.
This article aims to share an example of code refactoring with TypeScript discriminated union types and Generics.
The original code
This piece of code represents user roles and feature/permissions mapping, which is used to build a user access matrix in an Angular App.
The code above contains the following:
- FeatureAccess: A constant representing two features (“
UserManagement
” and “Order
”) and their permissions. - SalesRole: A Role class that describes the list of permissions a Sales user has and
“canAccess
” method based on that.
Issues
What are the problems with the above code?
Firstly, the return type of functions
Getter in SalesRole
Class is not strong enough. In the IUserPermission
interface, the permissions are a string array. Although it is clearly stated in FeatureAccess
constant that each feature has different permission (i.e. Order feature does not have Create/Delete Permission), if someone adds a “Create” permission into the SalesRole functions
Getter, the compiler will not complain.
Secondly, the code is not DRY. There are many repeated “namespace” codes like “FeatureAccess.UserManagement.Permissions.xxx
”, it is used to reduce the chance of incorrectly adding wrong permission into a role function list. In a real-world project with lots of roles and permissions, it makes the code hard to read.
My first attempt to refactor the code is to use the Discriminated union type.
Discriminated union type
A discriminated union type differentiates the types from each other via a property called discriminator. The power of discriminated type is that the additional type checking is enforced at compile time and also carries the type info into run time.
By applying the discriminated union type, the code is refactored as follows.
As the above code shows, the new UserTypes
and OrderTypes
are union types that represent the allowed permissions for each feature. The new functionList
type is a discriminated union type that enforces the relationship between the feature and its permissions.
Now, if someone adds a Create
permission to the Order
feature, the compiler will not be happy.
The repeat “namespace” code is removed because we don’t have to use the naming conversion to make it “safer,” and instead, with much better compiler type checking, we can use the simple PermList
constant and not need to worry about adding the wrong permission into the role function list.
As Const
Another thing worth highlighting is the “Const Assertion” being used here.
The as const
is the const assertion expression. It has two effects, make all the properties readonly
and prevent the string literal widening. The latter is an often forgotten feature but can be super useful sometimes. For example, without this magic expression, the PermList.View
will be a string instead of a literal “View” type, then the whole discriminated type won’t work for this case.
And it is not the end. We can improve the code further by using Generics in TypeScript.
Generics
You may already notice that the IUserFunctions
and IOrderFunctions
are similar. They all have the “name” and “permissions” properties. Again in the real world, those interfaces can grow quickly as well as duplicates.
As you can see, it is much cleaner after the Generics is applied.
Conclusion
The TypeScript Type system is powerful. This use case touches the surface, and I hope it can trigger your interest in exploring it further.
Happy programming!
- Title: Use Cases For TypeScript Discriminated Union Types and Generics
- Author: Sunny Sun
- Created at : 2020-02-28 00:00:00
- Updated at : 2024-08-16 19:46:17
- Link: http://coffeethinkcode.com/2020/02/28/a-practical-usage-of-typescript-discriminated-union-type-and-generics/
- License: This work is licensed under CC BY-NC-SA 4.0.