Hone logo
Hone
Problems

Enforcing Business Logic with Branded Types in TypeScript

This challenge focuses on a powerful TypeScript technique called "branded types" (also known as nominal typing or opaque types). Branded types allow you to create distinct types that share the same underlying primitive but are not assignable to each other, effectively enforcing specific business rules at compile time. This prevents common bugs by ensuring that values meant for one purpose are not accidentally used for another.

Problem Description

Your task is to implement a system for managing different types of identifiers within an application. You will create distinct "branded" types for UserId, ProductId, and OrderId. These identifiers, while internally represented as strings, should not be directly interchangeable. For example, a UserId should not be assignable to a ProductId, even though both are strings. You will then create functions that accept and return these specific branded types, demonstrating their type safety.

Key Requirements:

  1. Create Branded Types: Define UserId, ProductId, and OrderId as branded types. Each should be an alias for string but with a unique branding property.
  2. Type Safety: Ensure that a UserId cannot be passed to a function expecting a ProductId, and vice-versa.
  3. Constructing Branded Types: Implement a safe way to create instances of these branded types from plain strings, ensuring that the branding is applied correctly.
  4. Function Signatures: Create example functions that clearly demonstrate the usage of these branded types in their parameters and return values.

Expected Behavior:

  • TypeScript should prevent direct assignment of a string to a branded type (e.g., const userId: UserId = "user-123"; should be a type error unless explicitly cast).
  • TypeScript should prevent assignment between different branded types (e.g., const productId: ProductId = userId; should be a type error).
  • Functions designed to work with specific branded types should only accept those types.

Edge Cases:

  • Consider how to handle creating branded types from potentially invalid string inputs (though for this challenge, we'll assume valid string inputs for creation).
  • The primary focus is on compile-time type safety, not runtime validation of the string content itself.

Examples

Example 1:

// Define branded types
type UserId = string & { __brand: "UserId" };
type ProductId = string & { __brand: "ProductId" };

// Function to create a UserId (safe constructor)
function createUserId(id: string): UserId {
  return id as UserId; // Type assertion is acceptable within a controlled constructor
}

// Function to create a ProductId (safe constructor)
function createProductId(id: string): ProductId {
  return id as ProductId;
}

// Function that processes a UserId
function getUserName(userId: UserId): string {
  // In a real app, this would fetch data. Here, we just simulate.
  return `User with ID ${userId}`;
}

const userIdString = "user-abc-789";
const userId: UserId = createUserId(userIdString);
const productNameString = "prod-xyz-101";
const productId: ProductId = createProductId(productNameString);

console.log(getUserName(userId)); // Output: User with ID user-abc-789

// The following lines should cause TypeScript errors:

// const invalidUserId: UserId = "just a string"; // Error: Type '"just a string"' is not assignable to type 'UserId'.
// const assignProductIdToUserId: UserId = productId; // Error: Type 'ProductId' is not assignable to type 'UserId'.

Explanation: We define UserId and ProductId as strings with unique branding properties. We then create "safe constructors" (createUserId, createProductId) that use type assertions to convert plain strings into our branded types. The getUserName function specifically accepts a UserId. Attempts to assign plain strings or other branded types to UserId result in compile-time errors.

Example 2:

// Assuming UserId and ProductId are defined as in Example 1

type OrderId = string & { __brand: "OrderId" };

function createOrderId(id: string): OrderId {
  return id as OrderId;
}

function processOrder(orderId: OrderId, userId: UserId): string {
  return `Processing order ${orderId} for user ${userId}`;
}

const userId: UserId = createUserId("user-def-456");
const productId: ProductId = createProductId("prod-ghi-789");
const orderId: OrderId = createOrderId("order-jkl-012");

console.log(processOrder(orderId, userId)); // Output: Processing order order-jkl-012 for user user-def-456

// The following lines should cause TypeScript errors:

// console.log(processOrder(userId, orderId)); // Error: Argument of type 'UserId' is not assignable to parameter of type 'OrderId'.
// console.log(processOrder(productId, userId)); // Error: Argument of type 'ProductId' is not assignable to parameter of type 'OrderId'.

Explanation: We introduce OrderId and a processOrder function that requires both an OrderId and a UserId. This further demonstrates that even when functions accept multiple branded types, TypeScript enforces that the correct type is passed to each parameter.

Constraints

  • Type System: All solutions must be implemented using TypeScript.
  • No Runtime Type Checking: The solution should primarily rely on TypeScript's static type checking. No runtime checks (e.g., typeof or instanceof on the brand property) are required for the core challenge.
  • Clarity: The branded types and their usage should be clear and easy to understand.
  • Efficiency: The solution should not introduce significant runtime overhead. Branded types are a compile-time feature.

Notes

  • The __brand property is a common convention, but any unique, impossible-to-collide property name can be used. The key is that the property name and its literal type are unique across the different branded types.
  • When creating branded types from existing values (e.g., from an API response), you will often need to use a type assertion (as). The goal is to encapsulate this assertion within controlled factory functions or specific contexts where you can be reasonably sure of the input's validity.
  • Consider how you might extend this pattern to other scenarios where you need to distinguish between logically different but structurally similar data types.
Loading editor...
typescript