Enforcing Type Safety with Phantom Types for Units of Measurement
Phantom types are a powerful technique in TypeScript that allow you to add type information to a value without altering its runtime representation. This challenge focuses on leveraging phantom types to create a robust and type-safe system for handling units of measurement, preventing common errors like adding meters to kilograms.
Problem Description
Your task is to implement a system for representing and operating on physical quantities that have associated units of measurement. You will use phantom types to distinguish between different units at compile time, ensuring that operations are only allowed between compatible units.
Key Requirements:
- Unit Representation: Define a generic
Unittype that can represent various physical units (e.g.,Meter,Second,Kilogram). - Quantity Representation: Create a generic
Quantity<T extends Unit>type that wraps a numeric value and its associated unit. - Type Safety: Implement methods or functions for common operations (addition, subtraction, multiplication, division) that enforce unit compatibility at compile time. For example, you should not be able to add a
Quantity<Meter>to aQuantity<Second>. - Unit Conversions (Optional but Recommended): Explore how phantom types can be extended to handle unit conversions. For this core challenge, focus on ensuring operations are between identical units.
- Readability: The resulting code should be clear and easy to understand.
Expected Behavior:
- Operations between quantities of the same unit should succeed.
- Operations between quantities of different units should result in a compile-time error.
- The runtime value should remain a simple number.
Edge Cases:
- Operations involving zero.
- Division by zero (should result in a runtime error, not a compile-time one).
Examples
Example 1: Adding Quantities of the Same Unit
// Define units
interface Meter extends Unit {}
interface Second extends Unit {}
// Create quantities
const distance: Quantity<Meter> = new Quantity(10, 'm');
const anotherDistance: Quantity<Meter> = new Quantity(5, 'm');
// Operation
const totalDistance = distance.add(anotherDistance);
// Expected Output (runtime):
// Quantity { value: 15, unit: 'm' }
// Expected Output (compile-time):
// No errors. totalDistance should be of type Quantity<Meter>.
Example 2: Attempting to Add Quantities of Different Units
// Define units
interface Meter extends Unit {}
interface Second extends Unit {}
// Create quantities
const distance: Quantity<Meter> = new Quantity(10, 'm');
const time: Quantity<Second> = new Quantity(5, 's');
// Operation
// const invalidAddition = distance.add(time); // This should cause a compile-time error
// Expected Output (compile-time):
// TypeScript Error: Argument of type 'Quantity<Second>' is not assignable to parameter of type 'Quantity<Meter>'.
Example 3: Multiplication of Quantities
// Define units
interface Meter extends Unit {}
interface Second extends Unit {}
interface MeterPerSecond extends Unit {} // Derived unit
// Create quantities
const distance: Quantity<Meter> = new Quantity(10, 'm');
const time: Quantity<Second> = new Quantity(5, 's');
// Operation
// const speed: Quantity<MeterPerSecond> = distance.divide(time); // This should be a valid operation if implemented
// Expected Output (compile-time):
// speed should be of type Quantity<MeterPerSecond> (or some representation of it).
// For this core challenge, focus on ensuring distance.add(time) errors.
Constraints
- The core implementation should focus on ensuring compile-time safety.
- Runtime operations should be efficient.
- The
unitproperty can be a string representing the unit for simplicity, but the phantom type will be the primary enforcement mechanism.
Notes
- Think about how to declare your
Unitinterface andQuantityclass/interface such that they can accept a generic type parameter representing the specific unit. - Consider using branded types or intersection types for more advanced unit representations, but for this challenge, a simple interface is sufficient.
- The goal is to prevent runtime errors that would occur if you naively added different units.
- For operations like multiplication and division, you'll need to consider how to represent derived units. This can be complex, so focus on the core addition/subtraction safety first.