-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
241: Modeling Permissions with Literal Union Types
When you're building an app with different user levels—like a CMS or a project management tool—your first instinct might be to just use a string for the user's role. I've seen this in countless code reviews. It seems easy, but it's a recipe for runtime bugs that are a nightmare to track down.
Why bother with literal unions instead of just using string?
If you define a role as type Role = string, TypeScript is happy as long as you provide any text. But in the real world, you don't have "any" role; you have a specific set of them. If you accidentally type "admn" instead of "admin" in a permission check, a string type won't blink. Your user just won't be able to access the dashboard, and you'll spend an hour debugging why the if statement is failing.
By using a literal union, you're telling TypeScript that the value isn't just any string—it's exactly one of these specific strings. Check this out:
type UserRole = 'admin' | 'editor' | 'viewer';
interface User {
id: string;
role: UserRole;
}
const user: User = {
id: 'u123',
role: 'admn' // ❌ Error: Type '"admn"' is not assignable to type 'UserRole'.
};
Now the compiler is acting as your safety net. It catches the typo before the code even leaves your editor. I love this because it turns a potential production bug into a red squiggly line in VS Code.
How do I actually use these to restrict access in my functions?
The real magic happens when you combine these unions with TypeScript's ability to "narrow" types. Since the compiler knows exactly which strings are possible, it can help you write logic that is logically sound.
Let's say we have a function that handles deleting a document. Only admins and editors should be able to do that. You don't need fancy validation libraries here; a simple switch or if will do:
function canDeleteDocument(role: UserRole): boolean {
switch (role) {
case 'admin':
case 'editor':
return true;
case 'viewer':
return false;
}
}
Notice how I didn't need a default case here. Because UserRole is a union of those three specific strings, TypeScript knows that once you've handled 'admin', 'editor', and 'viewer', there's nothing left. If you try to return something outside that logic, TS will let you know.
How can I stop myself from forgetting a role when the app grows?
Here is where things get interesting. Imagine your boss comes to you in three months and says, "We need a moderator role." You add 'moderator' to your UserRole union, but you forget to update the canDeleteDocument function. Suddenly, moderators are hitting a wall (or worse, getting unintended access).
To prevent this, I use a pattern called "exhaustive checking" with the never type. It's a bit of a pro move, but it's incredibly powerful:
function canDeleteDocument(role: UserRole): boolean {
switch (role) {
case 'admin':
case 'editor':
return true;
case 'viewer':
return false;
default:
// This ensures that if a new role is added to the union,
// this line will throw a compile-time error.
const _exhaustiveCheck: never = role;
return _exhaustiveCheck;
}
}
By assigning role to a variable of type never in the default block, you're telling TypeScript: "I expect to have handled every single possibility by the time I get here." If you add 'moderator' to the union, role is no longer never—it's 'moderator'—and the code won't compile until you add a case for it. It basically forces you to be thorough.
📋 Practical Task
Exercise: Implementing a File System Access Guard
You are building a cloud storage application. You need to implement a permission system that handles three roles: 'owner', 'contributor', and 'guest'.
Your Task:
- Create a literal union type called
AccessLevelcontaining the three roles mentioned above. - Create a function called
getPermissionsthat takes anAccessLevelas an argument and returns a string description of what that user can do:'owner': "Full access: read, write, and delete."'contributor': "Limited access: read and write."'guest': "Read-only access."
- Implement an exhaustive check using the
nevertype in thedefaultcase of your switch statement to ensure that any future roles added toAccessLevelmust be handled in this function.
There are no comments for now.