TypeScript Discriminated Unions and Generics: Complete Guide with Examples
JUSTC0DE_SESSION002
Advanced20 minWelcome to the second session!
As developers, we sometimes need a mechanism for narrowing the type based on the assigned value wired to another in the same object or an array. This is where discrimination and generic types shines 💫
In today's article, we will talk about the type discrimination with Typescript to eliminate and reduce a set of potential objects down to one specific object as well as utilizing generic types in order to provide type-safe DX and a reduced, error-prone system at scale.
Enjoy reading!
Table of Contents
What are the terms 'discrimination' and 'generic'?
The term discrimination(/dɪˌskrɪmɪˈneɪʃn/) is the process of making distinctions between people based on groups, classes and the categories. It came out in the early 17th century in the English language to distinguish particular characteristics such as race, gender, age, class and so on.
In the Typescript language, this has been passed down as a way of categorizing and granting privilege in the associated context.
Meanwhile the term generic(/dʒɪˈnɛrɪk/) refers something general, common, or not specific just like a 'generic name' for a class of things such as 'generic brand'. The context when it comes to computer programming indicates a 'generic procedure'.
In other words the generics is a style of coding where algorithms and data structures are written in terms of types that are specified later, at the time the code is used.
Practicing with discriminated unions
A common practice I've seen in the community is distinguishing vehicle properties. You know like if the vehicle is assigned for a motorbike then you should not access the door prop in that object but I would rather address something more common like authorization — assigning privileges that define what a user can do.
Assuming that we're working on a dashboard app and have to deal with role-based access control(RBAC). Some of the roles likely have same or similar duties. A common dashboard app typically contains sections with standalone inner-modules and there must be restrictions for that particular sections depending on the user role. Therefore some of the sections are prohibited. Nevertheless, an employee may likely access what an editor can't — at least in my imaginary app.
In this case, we must implement our code based on the roles, so let's start with defining the Role literal type,
Say we've granted these privileges for each user type:
- guest demo sections such as dashboard, settings.
- user user-based sections such as profile management, order history.
- editor content management sections such as article editing, media library uploads and publications.
- employee employee-specific tools like timesheets, internal directories and everything that editor can access.
- admin simply put, everything mentioned above plus say finance.
Now, we're going to need that sections too,
The most primitive instances before we move on designing the interfaces in union type of literal string types. These are I think pretty enough for now to build the interfaces.
It's worth to mention that of course we won't get into the weeds like nested objects inside the interface so the following examples are simplified and does not reflect to the real world scenario of RBACs.
It's now the time to define our user interfaces,
So now everything looks fine so far, we have our interfaces that has their own props taking the advantages of lovely Extract util type in order to process the sections. The Employee gets whatever an editor has, so we don't need to rewrite everything from scratch. At the end of the article, I'll polish the implementation but for now it can stay as it is.
Looks cool, doesn't it? If you say so, then I suggest you check the code above once again. Because there a couple of bottlenecks in the code.
Before breaking it down, let's look at an example of a discriminated union to create a UserProfile.
Using it in our app,
And that's all! Now, our IDE suggest the proper sections following to the defined value of role prop.
But as I've mentioned earlier. There are still bottlenecks:
First bottleneck: Deduplication of tuples
When we attempt to assign section types to the User object, we encounter an indefinite unnecessary type recommendations that are already written, which causes an endless loop.

Second bottleneck: The hidden unresolved union types
Similarly, in this case when we attempt to assign a new value to the allowed prop belonging to the Editor interface, such as the media or articles sections, right after setting the role to employee like role: "employee" ts does his job by immediately signaling us about the root error that is shown below:

But why it's like that?
The root cause of deduplicated tuples
In order to avoid deduplication, we must turn the <Extract<Section, [literals]>> util type to something that can trim the used values. When a duplication present, the developer must be warned by the compiler. So our general desired intention is to force TypeScript to error when duplicates are present at compile time.
And to do that, we have to model the allowed property not as a general array type, but as a Tuple of Unique Values.
I addressed this issue in the next chapter as the logic contains generic type enforcements. Therefore, it would be better for me to cover the topic comprehensively in the next chapter.
Solution for the hidden unresolved union types
There is a small bug in the ts file that we wrote. I think unlike the previous issue, this one is more certain — somewhere inside the Employee interface. Here once again, check it out.
Take a look at this part:
Reminding the TS error Type '"articles"' is not assignable to type '"timesheets" | "directory"', even though the allowed prop is narrowed the Section type by inheriting Editor itself. We supposedly had everything what Editor has plus those 2 literals "timesheets" | "directory" but instead we allowed only for those 2 literals to use.
The underlying root issue was hidden over here:
To fix it, we need to add [number] at the end:
Then the correction yields a proper type experience:

We finally reached our goal by making a small change to our Employee interface. The final source type, yields like below:
Now it's time for the developers to build the business logic since we've prepared the type-safe background for the implementation 🥳
I tricked you 🫠
We have to go way too much to reach our desired goal. Remember our first issue? I cover it comprehensively in the rest of the article.
Making a generic type
Before moving on to the more sophisticated, unresolved issue that we postponed addressing under this heading associated with the first bottleneck, I'd like to review the fundamentals of enforcing generic types correlated with the deduplicated tuples as this topic requires general knowledge of specific baselines like:
- conditional types
- mapped types
- recursion
- inferring
and so on.
This is where we start discussing more about advanced Typescript, which is why I marked this article as advanced. But it's all good. I'll try to be as thorough as possible gradually and gracefully.
Creating basic generics
Type manipulation is difficult...
Therefore, developers are more likely to use traditional methods, such as creating an interface with built-in utility types like <Pick> and <Record>, in their app logic unless they are working on a library API or more static, business-driven logic.
This is why generics are used at the top of internal APIs to improve the developer experience (DX).
And I agree to disagree with the statement "Just use ts-pattern, or third-party libs bro."
I don't want to see a 452 kB package in my package.json/dependencies or package.json/devDependencies just for a utility excluding such scenarios when building something from scratch, and everything pushes you to utilize third-parties.
Returning to the topic, generics emerged as a way of capturing the type of the argument in a way that could also be used to denote the return value.
Therefore, simply put: a generic indicates special kind of variable that works on types rather than values.
Remember about the Section type we've mentioned above. First, let's try to shortening the previous implementation using generic types while respecting to the DRY principle:
T is indeed used as placeholder for Type. This acronym might be used arbitrarily. For the sake of what we address, I prefer using S as we go through Section type that we declared recently.
Usage:
It would be uncertain with the logic above and here is the proof:
So we must narrow the type depending on the context requirements and in our case its going to be Section type. To convert the logic we can utilize the extends keyword.
Definition: extends is widely used to extend interfaces, enabling the creation of more specific interfaces by inheriting properties and methods from a base interface.
The result yields:

We can also use the generics in our type to avoid repetition when implementing the Exclude util in our main interfaces like this:
You can think like this recent ExtractSection implementation is a total non-sense or overengineering. However consider that, as the app scales, it will likely facilitate the development process — especially with regard to reusability.
Typescript contitional types
Implements logic at the type level based on structural matching. Think it like ES6+ ternary operations over (if / else) statement:
Then we can implement it to our Section logic like:

Using 'infer' keyword to extract something from a matched type
We're going to need it soon, so addressing this topic was absolutely essential. The infer lets us capture parts of a matched type inside the conditional.
So how exactly?
if T is Promise<Something>, return that Something and otherwise return T.
A magic trick let us to extract — in my words expose the underlying internal logic. I don't know how many times I've encountered such scenarios that I had to work with the infer keyword especially in my open source projects. Use this guys.
But what does it yield? I haven't answered yet.
Now its time for the real-world implementation. I want you to go right back to the very first bottleneck, just to remember then scroll down over here to see how we enforce the uniqueness among tuples in Typescript to prevent deduplication. But no pressure if you remember, let's dive.
The comprehensive solution for the deduplicated tuples

We get too much work to do...
The first thing we need is a type-level utility that will check for duplication.
Say VerifyUnique:
S stands for acronym to Section — section literals:
readonly ensures that the literals are constant and static. I also used unknown[] since our application can use it for other literals without getting stuck on just one kind of type like Section[].
I know, sophisticated usage of generic types and combining all things I've mentioned in this article was Just for demonstration so far. We did not scale it yet.
If we need the same implementation elsewhere in our app, we remain faithful to the DRY principle. Thus, the benefits of maintainability and scalability comes under the hood.
Now, allow me to tell what is happening in the snippet above:
- Destruction the types using infer keyword.
We actually preparing the type by destructuring for the next recursion step. Think it something like:
2.Conditional checking by extending the destructed type
Checking the proceeding tuple or whatever you proceed at compile-time and making comparison for the rest tuple.
3. Recursive conditional type checking
If the given statement yields true then we assure that the duplication is truly exist inside the user-provided array. The problem is that I do not know why this error is not printing on the dev IDE even though the condition errors...
And finally our type-level utility with test context:
Critical point emerges when we use S & VerifyUnique<S>:

Alternative to print our custom ts error but fragile:

And when we omit the readonly, the logic completely loses its type-safety:

Using S[] yields:

I think the illustrations are good enough to break down distinctions between the usages with the util-type VerifyUnique.
So let's move on the final boss
Creating a runtime function that wraps everything.
We figured out the 75% of the problem. Now, we're going to need a function that wraps the logic itself and enable us to create users.
I'll go gradually this time.
Say createUser:
Let's now, embed the UserProfile we have prepared in the very beginning of this article:
Cool, now time to transform it to something that can be consumed by VerifyUnique typescript util-type we created.
Eventual breakage likely happens exactly on the following line and I guess for the rest, no need to explain as we mentioned the details on the rest of the article context.
Step by step If we break it down:
- At the call site S is the actual tuple type we pass S = readonly ["dashboard", "orders"].
- VerifyUnique<S> ensures that the uniqueness across the wired intersections.
- At the end the allowed value MUST BE ASSIGNIBLE and MUST SATISFY every constraint simultaneously.
And at the end the allowed sections must be:
- The literal tuple type S,
- Pass VerifyUnique<S> (i.e., be unique),
- Match the role-specific allowed shape.
Bottom Line
Below, code snippet works seamlessly with Typescript throughout every possible way that satisfies createUser:
It's relatively E2E, but we needed to add branded types for ID to make the fully transparent E2E enforcement. Let's discuss that in other sessions though, since I'm pretty exhausted from writing this article.
Now I leave you with the result of all that effort and struggle 🥳💫:

Wuuh.. 😅. I think this session was the most technical one so far(among 2 article in total :P). It was painful for me tbh. Because the more I wanted to address further subjects the more I've struggled.
I hope you liked the content. Today we were talking about TypeScript Discriminated Unions and Generics: Complete Guide with Examples. Subscribe to the newsletter to get instant updates. If you want to support me in order to keep these long sessions coming, you can buy me a coffee.
Thanks from now for your all supports and see you in the next one ❤️