Master TypeScript's enums, literal types, and brand types for creating precise, self-documenting APIs. Learn when to use each approach and how they improve type safety and code clarity.
Enums, literal types, and brand types are TypeScript's tools for creating precise, constrained types that go beyond basic primitives. They enable you to model domain concepts accurately, create self-documenting APIs, and catch errors at compile time.
These features are essential for building maintainable applications where values have specific meanings and constraints. You'll learn when to use each approach, their performance characteristics, and how they integrate with TypeScript's type system.
By mastering these concepts, you'll be able to create APIs that are both type-safe and expressive, making your code more robust and easier to understand.
Enums provide a way to define named constants and create a type that can only be one of those predefined values.
Numeric Enums:
1// Basic numeric enum - values auto-increment from 0
2enum Direction {
3 Up, // 0
4 Down, // 1
5 Left, // 2
6 Right // 3
7}
8
9// Using the enum
10function move(direction: Direction): void {
11 switch (direction) {
12 case Direction.Up:
13 console.log("Moving up");
14 break;
15 case Direction.Down:
16 console.log("Moving down");
17 break;
18 case Direction.Left:
19 console.log("Moving left");
20 break;
21 case Direction.Right:
22 console.log("Moving right");
23 break;
24 }
25}
26
27move(Direction.Up); // Valid
28move(Direction.Left); // Valid
29// move(5); // Error: not assignable to Direction
30
31// Custom numeric values
32enum HttpStatus {
33 OK = 200,
34 BadRequest = 400,
35 Unauthorized = 401,
36 Forbidden = 403,
37 NotFound = 404,
38 InternalServerError = 500
39}
40
41function handleResponse(status: HttpStatus): string {
42 switch (status) {
43 case HttpStatus.OK:
44 return "Request successful";
45 case HttpStatus.BadRequest:
46 return "Invalid request";
47 case HttpStatus.Unauthorized:
48 return "Authentication required";
49 case HttpStatus.NotFound:
50 return "Resource not found";
51 default:
52 return "Unknown status";
53 }
54}String Enums:
1// String enums provide better debugging and serialization
2enum LogLevel {
3 DEBUG = "debug",
4 INFO = "info",
5 WARN = "warn",
6 ERROR = "error"
7}
8
9enum Color {
10 RED = "red",
11 GREEN = "green",
12 BLUE = "blue",
13 YELLOW = "yellow"
14}
15
16// String enums are serialization-friendly
17function log(level: LogLevel, message: string): void {
18 const timestamp = new Date().toISOString();
19 console.log(`[${timestamp}] [${level}] ${message}`);
20}
21
22log(LogLevel.INFO, "Application started");
23log(LogLevel.ERROR, "Database connection failed");
24
25// JSON serialization preserves string values
26const config = {
27 logLevel: LogLevel.DEBUG,
28 theme: Color.BLUE
29};
30
31console.log(JSON.stringify(config));
32// Output: {"logLevel":"debug","theme":"blue"}Const Enums for Performance:
1// Const enums are inlined at compile time
2const enum Environment {
3 DEVELOPMENT = "development",
4 STAGING = "staging",
5 PRODUCTION = "production"
6}
7
8const enum Feature {
9 USER_MANAGEMENT = 1,
10 ANALYTICS = 2,
11 NOTIFICATIONS = 4,
12 PREMIUM = 8
13}
14
15// Usage is inlined - no runtime enum object
16function getApiUrl(env: Environment): string {
17 switch (env) {
18 case Environment.DEVELOPMENT: // Becomes "development"
19 return "http://localhost:3000/api";
20 case Environment.STAGING: // Becomes "staging"
21 return "https://staging-api.example.com";
22 case Environment.PRODUCTION: // Becomes "production"
23 return "https://api.example.com";
24 }
25}
26
27// Bitwise operations with const enums
28function hasFeature(userFeatures: number, feature: Feature): boolean {
29 return (userFeatures & feature) === feature;
30}
31
32const userPermissions = Feature.USER_MANAGEMENT | Feature.ANALYTICS;
33console.log(hasFeature(userPermissions, Feature.ANALYTICS)); // trueComputed and Mixed Enums:
1// Enums with computed values
2enum FileAccess {
3 None,
4 Read = 1 << 1, // 2
5 Write = 1 << 2, // 4
6 ReadWrite = Read | Write, // 6
7 Execute = 1 << 3 // 8
8}
9
10function checkAccess(access: FileAccess, required: FileAccess): boolean {
11 return (access & required) === required;
12}
13
14// Mixed string and numeric enum (not recommended)
15enum BooleanLikeHeterogeneousEnum {
16 No = 0,
17 Yes = "YES",
18}
19
20// Reverse mapping for numeric enums
21enum Status {
22 PENDING,
23 APPROVED,
24 REJECTED
25}
26
27console.log(Status[0]); // "PENDING"
28console.log(Status["PENDING"]); // 0
29console.log(Status[Status.APPROVED]); // "APPROVED"Enum Utilities and Patterns:
1// Enum utility functions
2function getEnumValues<T extends Record<string, string | number>>(
3 enumObject: T
4): Array<T[keyof T]> {
5 return Object.values(enumObject);
6}
7
8function getEnumKeys<T extends Record<string, string | number>>(
9 enumObject: T
10): Array<keyof T> {
11 return Object.keys(enumObject).filter(key => isNaN(Number(key))) as Array<keyof T>;
12}
13
14function isValidEnumValue<T extends Record<string, string | number>>(
15 enumObject: T,
16 value: any
17): value is T[keyof T] {
18 return Object.values(enumObject).includes(value);
19}
20
21// Usage
22enum Priority {
23 LOW = "low",
24 MEDIUM = "medium",
25 HIGH = "high",
26 CRITICAL = "critical"
27}
28
29const priorities = getEnumValues(Priority); // ["low", "medium", "high", "critical"]
30const priorityKeys = getEnumKeys(Priority); // ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
31
32function setPriority(priority: string): void {
33 if (isValidEnumValue(Priority, priority)) {
34 console.log(`Setting priority to: ${priority}`);
35 } else {
36 console.log("Invalid priority value");
37 }
38}
39
40setPriority("high"); // Valid
41setPriority("urgent"); // InvalidLiteral types represent exact values rather than general types, enabling precise APIs and compile-time value checking.
String Literal Types:
1// String literal types
2type Theme = "light" | "dark";
3type Size = "small" | "medium" | "large";
4type Alignment = "left" | "center" | "right";
5
6interface ButtonProps {
7 text: string;
8 size: Size;
9 theme: Theme;
10 align?: Alignment;
11}
12
13function createButton(props: ButtonProps): void {
14 console.log(`Creating ${props.size} ${props.theme} button: ${props.text}`);
15}
16
17createButton({
18 text: "Click me",
19 size: "medium",
20 theme: "dark"
21}); // Valid
22
23// createButton({
24// text: "Invalid",
25// size: "tiny", // Error: not assignable to Size
26// theme: "dark"
27// });
28
29// Template literal types
30type EventName<T extends string> = `on${Capitalize<T>}`;
31type MouseEventName = EventName<"click" | "hover" | "focus">; // "onClick" | "onHover" | "onFocus"
32
33type Route = `/${string}`;
34type ApiRoute = `/api/${string}`;
35
36function navigateTo(route: Route): void {
37 console.log(`Navigating to: ${route}`);
38}
39
40navigateTo("/dashboard"); // Valid
41navigateTo("/api/users"); // Valid
42// navigateTo("invalid"); // Error: must start with /Numeric Literal Types:
1// Numeric literal types
2type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
3type HttpSuccessStatus = 200 | 201 | 202 | 204;
4type Port = 3000 | 8080 | 9000;
5
6function rollDice(): DiceRoll {
7 return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
8}
9
10function startServer(port: Port): void {
11 console.log(`Server starting on port ${port}`);
12}
13
14startServer(3000); // Valid
15// startServer(3001); // Error: not assignable to Port
16
17// Combining with conditional types
18type StatusCode<T extends number> = T extends HttpSuccessStatus
19 ? "success"
20 : T extends 400 | 401 | 403 | 404
21 ? "client-error"
22 : T extends 500 | 502 | 503
23 ? "server-error"
24 : "unknown";
25
26type Status200 = StatusCode<200>; // "success"
27type Status404 = StatusCode<404>; // "client-error"
28type Status500 = StatusCode<500>; // "server-error"Boolean and Symbol Literal Types:
1// Boolean literal types
2type IsEnabled = true;
3type IsDisabled = false;
4
5interface FeatureFlag {
6 analytics: boolean;
7 darkMode: IsEnabled; // Must be true
8 betaFeatures: IsDisabled; // Must be false
9}
10
11const features: FeatureFlag = {
12 analytics: true,
13 darkMode: true, // Must be true
14 betaFeatures: false // Must be false
15};
16
17// Symbol literal types
18const BRAND_SYMBOL = Symbol("brand");
19type BrandSymbol = typeof BRAND_SYMBOL;
20
21interface BrandedType {
22 [BRAND_SYMBOL]: BrandSymbol;
23 value: string;
24}
25
26// Unique symbol types
27declare const UserIdBrand: unique symbol;
28type UserId = string & { [UserIdBrand]: never };
29
30declare const EmailBrand: unique symbol;
31type Email = string & { [EmailBrand]: never };
32
33function createUserId(id: string): UserId {
34 return id as UserId;
35}
36
37function createEmail(email: string): Email {
38 return email as Email;
39}
40
41function sendEmail(userId: UserId, email: Email): void {
42 console.log(`Sending email to user ${userId} at ${email}`);
43}
44
45const userId = createUserId("user-123");
46const email = createEmail("[email protected]");
47
48sendEmail(userId, email);
49// sendEmail(email, userId); // Error: arguments swappedLiteral Type Narrowing:
1// Literal type narrowing
2function handleEvent(event: { type: "click" | "hover" | "focus" }): void {
3 switch (event.type) {
4 case "click":
5 console.log("Handling click event");
6 break;
7 case "hover":
8 console.log("Handling hover event");
9 break;
10 case "focus":
11 console.log("Handling focus event");
12 break;
13 default:
14 // TypeScript ensures exhaustive checking
15 const exhaustiveCheck: never = event.type;
16 throw new Error(`Unhandled event type: ${exhaustiveCheck}`);
17 }
18}
19
20// Const assertions preserve literal types
21const config = {
22 apiUrl: "https://api.example.com",
23 timeout: 5000,
24 retries: 3
25} as const;
26// Type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3; }
27
28const themes = ["light", "dark", "auto"] as const;
29type ThemeOption = typeof themes[number]; // "light" | "dark" | "auto"
30
31// Function with const parameters
32function createTheme<T extends readonly string[]>(
33 themes: T
34): { [K in T[number]]: string } {
35 const result = {} as { [K in T[number]]: string };
36 themes.forEach(theme => {
37 result[theme] = `theme-${theme}`;
38 });
39 return result;
40}
41
42const themeClasses = createTheme(["primary", "secondary", "accent"] as const);
43// Type: { primary: string; secondary: string; accent: string; }Advanced Literal Type Patterns:
1// Mapped types with literal types
2type PermissionLevel = "read" | "write" | "admin";
3type Resource = "user" | "post" | "comment";
4
5type Permission = `${Resource}:${PermissionLevel}`;
6// Type: "user:read" | "user:write" | "user:admin" | "post:read" | etc.
7
8type PermissionMatrix = {
9 [K in Permission]: boolean;
10};
11
12const userPermissions: Partial<PermissionMatrix> = {
13 "user:read": true,
14 "user:write": true,
15 "post:read": true,
16 "post:write": false
17};
18
19// Type-safe permission checking
20function hasPermission(
21 permissions: Partial<PermissionMatrix>,
22 permission: Permission
23): boolean {
24 return permissions[permission] === true;
25}
26
27console.log(hasPermission(userPermissions, "user:write")); // true
28console.log(hasPermission(userPermissions, "post:admin")); // false
29
30// Dynamic literal type generation
31type SqlOperator = "=" | "!=" | ">" | "<" | ">=" | "<=";
32type WhereClause<T> = {
33 [K in keyof T]?: {
34 operator: SqlOperator;
35 value: T[K];
36 };
37};
38
39interface User {
40 id: number;
41 name: string;
42 age: number;
43 email: string;
44}
45
46const userQuery: WhereClause<User> = {
47 age: { operator: ">=", value: 18 },
48 name: { operator: "=", value: "John" }
49};
50
51function buildWhereClause<T>(where: WhereClause<T>): string {
52 return Object.entries(where)
53 .map(([key, condition]) => `${key} ${condition!.operator} '${condition!.value}'`)
54 .join(" AND ");
55}
56
57console.log(buildWhereClause(userQuery)); // "age >= '18' AND name = 'John'"Brand types (also called nominal types) create distinct types from the same underlying primitive, preventing accidental mixing of semantically different values.
Basic Brand Types:
1// Brand types using intersection with unique symbols
2declare const UserIdBrand: unique symbol;
3export type UserId = string & { readonly [UserIdBrand]: never };
4
5declare const EmailBrand: unique symbol;
6export type Email = string & { readonly [EmailBrand]: never };
7
8declare const PasswordBrand: unique symbol;
9export type Password = string & { readonly [PasswordBrand]: never };
10
11// Brand type constructors
12function createUserId(id: string): UserId {
13 if (!id || id.length < 3) {
14 throw new Error("Invalid user ID");
15 }
16 return id as UserId;
17}
18
19function createEmail(email: string): Email {
20 if (!email.includes("@")) {
21 throw new Error("Invalid email format");
22 }
23 return email as Email;
24}
25
26function createPassword(password: string): Password {
27 if (password.length < 8) {
28 throw new Error("Password must be at least 8 characters");
29 }
30 return password as Password;
31}
32
33// Type-safe function that prevents mixing up parameters
34function authenticateUser(userId: UserId, email: Email, password: Password): boolean {
35 console.log(`Authenticating user ${userId} with email ${email}`);
36 return true; // Simplified authentication
37}
38
39// Usage
40const userId = createUserId("user-12345");
41const email = createEmail("[email protected]");
42const password = createPassword("secretpassword");
43
44authenticateUser(userId, email, password); // Correct order
45// authenticateUser(email, userId, password); // Error: wrong parameter typesNumeric Brand Types:
1// Brand types for numeric values
2declare const PriceBrand: unique symbol;
3export type Price = number & { readonly [PriceBrand]: never };
4
5declare const QuantityBrand: unique symbol;
6export type Quantity = number & { readonly [QuantityBrand]: never };
7
8declare const PercentageBrand: unique symbol;
9export type Percentage = number & { readonly [PercentageBrand]: never };
10
11// Constructors with validation
12function createPrice(value: number): Price {
13 if (value < 0) {
14 throw new Error("Price cannot be negative");
15 }
16 return value as Price;
17}
18
19function createQuantity(value: number): Quantity {
20 if (value < 0 || !Number.isInteger(value)) {
21 throw new Error("Quantity must be a non-negative integer");
22 }
23 return value as Quantity;
24}
25
26function createPercentage(value: number): Percentage {
27 if (value < 0 || value > 100) {
28 throw new Error("Percentage must be between 0 and 100");
29 }
30 return value as Percentage;
31}
32
33// Business logic with type safety
34function calculateTotal(price: Price, quantity: Quantity): Price {
35 return createPrice(price * quantity);
36}
37
38function applyDiscount(price: Price, discount: Percentage): Price {
39 return createPrice(price * (1 - discount / 100));
40}
41
42// Usage
43const itemPrice = createPrice(29.99);
44const itemQuantity = createQuantity(3);
45const discountRate = createPercentage(15);
46
47const subtotal = calculateTotal(itemPrice, itemQuantity);
48const finalPrice = applyDiscount(subtotal, discountRate);
49
50console.log(`Final price: $${finalPrice}`);
51
52// This would cause a compile error:
53// calculateTotal(itemQuantity, itemPrice); // Error: parameters swappedBrand Types with Validation Classes:
1// More sophisticated brand types with validation
2class BrandedValue<T, B> {
3 private readonly _brand!: B;
4 constructor(public readonly value: T) {}
5
6 static create<T, B>(
7 value: T,
8 validator: (value: T) => boolean,
9 errorMessage: string
10 ): BrandedValue<T, B> {
11 if (!validator(value)) {
12 throw new Error(errorMessage);
13 }
14 return new BrandedValue<T, B>(value);
15 }
16}
17
18// Define brand types using the class
19type PhoneNumber = BrandedValue<string, "PhoneNumber">;
20type CreditCardNumber = BrandedValue<string, "CreditCardNumber">;
21type SocialSecurityNumber = BrandedValue<string, "SSN">;
22
23// Factory functions with validation
24function createPhoneNumber(phone: string): PhoneNumber {
25 return BrandedValue.create(
26 phone,
27 (p) => /^\+?[1-9]\d{1,14}$/.test(p),
28 "Invalid phone number format"
29 );
30}
31
32function createCreditCardNumber(ccn: string): CreditCardNumber {
33 return BrandedValue.create(
34 ccn,
35 (c) => /^\d{13,19}$/.test(c.replace(/\s/g, "")),
36 "Invalid credit card number"
37 );
38}
39
40// Usage with type safety
41function sendSMS(phoneNumber: PhoneNumber, message: string): void {
42 console.log(`Sending SMS to ${phoneNumber.value}: ${message}`);
43}
44
45function processPayment(cardNumber: CreditCardNumber, amount: Price): void {
46 console.log(`Processing payment of $${amount} with card ${cardNumber.value}`);
47}
48
49const phone = createPhoneNumber("+1234567890");
50const card = createCreditCardNumber("4111111111111111");
51const amount = createPrice(99.99);
52
53sendSMS(phone, "Your order has been confirmed");
54processPayment(card, amount);
55
56// This would cause a compile error:
57// sendSMS(card, "message"); // Error: wrong typeAdvanced Brand Type Patterns:
1// Brand types with additional metadata
2interface BrandedMetadata<T, B, M = {}> {
3 readonly value: T;
4 readonly brand: B;
5 readonly metadata: M;
6}
7
8type SafeUrl = BrandedMetadata<string, "SafeUrl", { protocol: "https" | "http" }>;
9type ValidatedDate = BrandedMetadata<Date, "ValidatedDate", { timezone: string }>;
10
11function createSafeUrl(url: string): SafeUrl {
12 const urlObj = new URL(url);
13 if (!["http:", "https:"].includes(urlObj.protocol)) {
14 throw new Error("Only HTTP and HTTPS URLs are allowed");
15 }
16
17 return {
18 value: url,
19 brand: "SafeUrl" as const,
20 metadata: { protocol: urlObj.protocol.slice(0, -1) as "https" | "http" }
21 };
22}
23
24function createValidatedDate(date: Date, timezone: string): ValidatedDate {
25 if (isNaN(date.getTime())) {
26 throw new Error("Invalid date");
27 }
28
29 return {
30 value: date,
31 brand: "ValidatedDate" as const,
32 metadata: { timezone }
33 };
34}
35
36// Generic brand type utilities
37type ExtractValue<T> = T extends BrandedMetadata<infer V, any, any> ? V : never;
38type ExtractBrand<T> = T extends BrandedMetadata<any, infer B, any> ? B : never;
39type ExtractMetadata<T> = T extends BrandedMetadata<any, any, infer M> ? M : never;
40
41function getValue<T extends BrandedMetadata<any, any, any>>(branded: T): ExtractValue<T> {
42 return branded.value as ExtractValue<T>;
43}
44
45function getMetadata<T extends BrandedMetadata<any, any, any>>(branded: T): ExtractMetadata<T> {
46 return branded.metadata as ExtractMetadata<T>;
47}
48
49// Usage
50const safeUrl = createSafeUrl("https://api.example.com");
51const validDate = createValidatedDate(new Date(), "UTC");
52
53console.log("URL value:", getValue(safeUrl));
54console.log("URL protocol:", getMetadata(safeUrl).protocol);
55console.log("Date timezone:", getMetadata(validDate).timezone);
56
57// Array of brand types
58type DatabaseId = BrandedValue<number, "DatabaseId">;
59type DatabaseIds = DatabaseId[];
60
61function createDatabaseId(id: number): DatabaseId {
62 return BrandedValue.create(
63 id,
64 (n) => Number.isInteger(n) && n > 0,
65 "Database ID must be a positive integer"
66 );
67}
68
69function batchDelete(ids: DatabaseIds): void {
70 console.log(`Deleting records with IDs: ${ids.map(id => id.value).join(", ")}`);
71}
72
73const ids: DatabaseIds = [
74 createDatabaseId(1),
75 createDatabaseId(2),
76 createDatabaseId(3)
77];
78
79batchDelete(ids);Comparison: Enums vs Literals vs Brands:
1// Scenario: User roles in different approaches
2
3// 1. Enum approach
4enum UserRoleEnum {
5 ADMIN = "admin",
6 USER = "user",
7 MODERATOR = "moderator"
8}
9
10// 2. Literal type approach
11type UserRoleLiteral = "admin" | "user" | "moderator";
12
13// 3. Brand type approach
14declare const UserRoleBrand: unique symbol;
15type UserRoleBranded = string & { [UserRoleBrand]: never };
16
17function createUserRole(role: UserRoleLiteral): UserRoleBranded {
18 return role as UserRoleBranded;
19}
20
21// Comparison of usage
22function checkPermissionsEnum(role: UserRoleEnum): boolean {
23 return role === UserRoleEnum.ADMIN;
24}
25
26function checkPermissionsLiteral(role: UserRoleLiteral): boolean {
27 return role === "admin";
28}
29
30function checkPermissionsBranded(role: UserRoleBranded): boolean {
31 return role === createUserRole("admin");
32}
33
34// Trade-offs:
35// Enums: Runtime object, reverse lookup, can be extended
36// Literals: No runtime overhead, great for unions, JSON-friendly
37// Brands: Type safety between different string types, validation at creation
38
39console.log("All approaches demonstrated for comparison");Your progress is saved in this browser, on this device only.