How to create the equivalent of Rust's enum variant classes, or kotlin sealed classes in typescript?

2 min read 05-10-2024
How to create the equivalent of Rust's enum variant classes, or kotlin sealed classes in typescript?


Mimicking Rust Enums and Kotlin Sealed Classes in TypeScript

In languages like Rust and Kotlin, enums and sealed classes offer a powerful way to model a finite set of possible values while also enforcing type safety. These features are particularly valuable when working with data that can only take on a limited number of states. TypeScript, while lacking direct equivalents, provides mechanisms to achieve similar behavior.

The Problem:

Let's say we're designing a system that manages user accounts. We want to represent the different account statuses like Active, Inactive, and Pending. In Rust or Kotlin, we'd use enums or sealed classes to define these states and ensure that a variable can only hold one of them at a time. However, TypeScript doesn't offer built-in constructs that directly match this pattern.

The Solution:

TypeScript's flexibility allows us to emulate the behavior of Rust enums or Kotlin sealed classes by employing a combination of techniques:

1. Using a Union Type:

We can create a union type to represent the possible account statuses:

type AccountStatus = 'Active' | 'Inactive' | 'Pending';

This declares a type AccountStatus that can only be one of the string literals 'Active', 'Inactive', or 'Pending'.

2. Defining Interfaces for Each Variant:

To add data associated with each status, we create interfaces for each variant:

interface ActiveAccount {
  type: 'Active';
  // Additional properties specific to Active accounts
}

interface InactiveAccount {
  type: 'Inactive';
  // Additional properties specific to Inactive accounts
}

interface PendingAccount {
  type: 'Pending';
  // Additional properties specific to Pending accounts
}

Each interface has a type property that identifies the variant, allowing us to distinguish between different account statuses.

3. Enforcing Type Safety with Conditional Types:

To ensure that the correct data is associated with each status, we can use conditional types:

type Account = 
  | ActiveAccount
  | InactiveAccount
  | PendingAccount;

function handleAccount(account: Account) {
  switch (account.type) {
    case 'Active':
      // Access account.additional properties for Active accounts
      break;
    case 'Inactive':
      // Access account.additional properties for Inactive accounts
      break;
    case 'Pending':
      // Access account.additional properties for Pending accounts
      break;
  }
}

This ensures that handleAccount only receives an object that conforms to one of the defined interfaces.

Example:

let activeAccount: ActiveAccount = {
  type: 'Active',
  email: '[email protected]',
  subscriptionLevel: 'Premium'
};

let inactiveAccount: InactiveAccount = {
  type: 'Inactive',
  reason: 'Billing issue'
};

handleAccount(activeAccount);
handleAccount(inactiveAccount); 

Benefits of this Approach:

  • Type Safety: Ensures that variables can only hold values of defined types.
  • Code Clarity: Enforces a clear distinction between different account statuses, improving code readability.
  • Data Integrity: Prevents inconsistencies by ensuring that each status has the appropriate data associated with it.

Additional Considerations:

  • Pattern Matching: While TypeScript doesn't have direct pattern matching like Rust, it's possible to achieve similar behavior using switch statements and type guards.
  • Discriminated Unions: This approach leverages the concept of discriminated unions, which are widely used in TypeScript to model data with multiple possibilities.

Conclusion:

While TypeScript doesn't have direct equivalents to Rust enums or Kotlin sealed classes, we can achieve similar functionality using union types, interfaces, and conditional types. This approach promotes type safety, code clarity, and data integrity, making it an effective way to model finite sets of possible values in TypeScript.