Safe Units

repository·master·Indexed 43 days ago

https://github.com/jscheiny/safe-units

A TypeScript library for type-safe dimensional analysis and units of measure. It ensures that unit mismatches in physical quantities like length, mass, and time are caught at compile-time. Version 2.0.1 supports SI and binary prefixes, custom unit systems, and arithmetic for dimensions with exponents between -5 and +5.

Tokens
15.5K
Snippets
43
Records
58
Agent score
45%

What's inside Safe Units

  1. What is a Measure and how does it work?

    master

    A Measure is an immutable value associated with a unit (e.g., 5 meters) within a specific unit system (e.g., SI).

    Key Characteristics:

    • Immutability: Any operation on a Measure returns a new Measure instance.
    • Type Safety: The Measure class uses two type parameters: B (the basis for the unit system) and U (the type of the unit).
    • Internal Representation: All measures are represented internally in terms of their base units (e.g., meters for length). This allows for seamless arithmetic between different units of the same dimension without manual conversion.
    • Numeric Type: By default, Measure works with JavaScript number types.
  2. How Unit Systems work in Safe Units

    master

    A Unit System defines a set of dimensions (the basis) and their corresponding base units. These dimensions can be combined through multiplication and division to form complex units.

    Safe Units provides a default implementation of the SI unit system, but allows developers to define custom unit systems by specifying a basis of dimensions and base units. This is useful for domain-specific requirements, such as game engines where you might need dimensions for 'frames' or 'engine units'.

  3. Use Prefix Functions for SI and Binary Prefixes

    master

    Instead of providing every possible combination of prefixes and units (e.g., kilometers, millimeters), Safe Units provides prefix functions. These functions operate on existing measures to construct a new measure with the corresponding symbol.

    Prefix functions are available for:

  4. Understand Built-in Units and Dimensions

    master

    Safe Units provides a comprehensive collection of built-in quantities and units based on the SI system. All quantities are generic, following the pattern type Quantity<N = number>, meaning they default to using number as the numeric type but can be configured with other types (like BigNumber).

    Standard SI Dimensions

    • Length (meters, m)
    • Mass (kilograms, kg)
    • Time (seconds, s)
    • ElectricCurrent (amperes, A)
    • Temperature (kelvin, K)
    • AmountOfSubstance (moles, mol)
    • LuminousIntensity (candelas, cd)

    Additional Dimensions

    • PlaneAngle (radians, rd)
    • SolidAngle (steradians, sr)
    • Memory (bits, b)
  5. Install safe-units

    master

    You can install safe-units using npm or yarn. This library requires TypeScript 3.2 or later and requires strictNullChecks to be enabled in your tsconfig.json to ensure full type safety.

    npm install safe-units
    # or
    yarn add safe-units
  6. Define Base Units and Quantities

    master

    A quantity is the type of a measurement (e.g., Length or Time). To define them:

    1. Use Measure.dimension to create a measure equivalent to 1 unit of a specific dimension.
    2. Create helper variables (e.g., const Length = engineUnits) to make deriving other quantities more expressive.
    3. Define the quantity type using the base unit. This ensures that any measurement representing that dimension (regardless of the specific unit used) is assignable to that quantity type.
    // Define the base unit of length in the system
    const engineUnits = Measure.dimension<GameUnitSystem, 'length'>();
    
    // Create a helper for expressive code
    const Length = engineUnits;
    
    // Define the quantity type
    type LengthQuantity = typeof Length;
  7. Wrap numeric functions for use with Measures

    master

    To avoid manually extracting values and re-wrapping them, use the provided function wrappers to convert standard numeric functions into measure-aware functions. These wrappers ensure that the unit is preserved or correctly transformed.

    Available wrappers:

    • wrapUnaryFn: Wraps a function of a single number (e.g., Math.abs).
    • wrapBinaryFn: Wraps a function of two numbers, returning a function that expects two measures of the same unit.
    • wrapSpreadFn: Wraps a function that takes any number of numbers and returns a function that takes one or more measures of the same unit.
    • wrapReducerFn: Wraps a function that takes two numbers and returns a function which takes one or more measures and performs a reduce across its inputs.
    // Example: Wrapping Math.abs to work on measures
    const absMeasure = wrapUnaryFn(Math.abs);
    const result = absMeasure(someMeasure); // Returns Measure<B, U>
  8. Create Base Units for non-number types

    master
    The default built-in base units (like meters, kilograms, etc.) are provided for number measures. If you are using a different numeric type, such as BigNumber, you can create corresponding base units for your specific measure type using the patterns demonstrated in the library's built-in base unit implementations.
  9. Derive complex Quantities and Units

    master

    Once base dimensions are defined, you can derive complex quantities (like acceleration or velocity) by combining existing measures. Because quantities are instances of Measure, you can use arithmetic operations to define them, allowing the code to mirror the mathematical definition.

    After defining the quantities, you can then define specific units within those quantities (e.g., defining metersPerSecond as a unit of Velocity).

    // Deriving a quantity (Velocity = Length / Time)
    const Velocity = Length / Time;
    
    // Defining a specific unit within that quantity
    const metersPerSecond = Velocity.from(1, 'm/s');
  10. Create a custom Unit System

    master

    To create a custom unit system, you must define a basis consisting of dimensions and their associated base units.

    To ensure clean IntelliSense in your editor, it is recommended to define the basis as an interface. You can then pass this interface as a type parameter to UnitSystem.from. This prevents your types from appearing as the complex internal object type of the basis and instead shows your custom interface name (e.g., GameUnitSystem).

    interface GameUnitSystemBasis {
      eu: Measure<GameUnitSystem, 'length'>;
      ms: Measure<GameUnitSystem, 'time'>;
      fr: Measure<GameUnitSystem, 'frames'>;
    }
    
    const GameUnitSystem = UnitSystem.from<GameUnitSystemBasis>({
      eu: dimension('length'),
      ms: dimension('time'),
      fr: dimension('frames'),
    });
  11. Derive complex quantities using unit arithmetic

    master

    You can derive complex physical quantities by performing arithmetic operations on base quantities. The library provides methods like .times(), .over() (division), .inverse(), .squared(), and .cubed() to compose new units. This ensures that the resulting type correctly reflects the derived dimension.

    Examples of derived quantities:

    • Velocity: Length.over(Time)
    • Acceleration: Velocity.over(Time)
    • Force: Mass.times(Acceleration)
    • Energy: Force.times(Length)
    • Power: Energy.over(Time)
    • Pressure: Force.over(Area)
    • Frequency: Time.inverse()
    import { Length, Time, Velocity, Acceleration } from "safe-units";
    
    // The library provides pre-defined constants for these derived types
    const speed: Velocity = Velocity(299792458);
    const gravity: Acceleration = Acceleration(9.80665);
  12. Customize unit symbols and prefixes

    master

    You can define custom symbols for units using .withSymbol("symbol"). You can also use built-in prefix functions like milli(), mega(), and micro() to create scaled units. The .in(targetUnit) method converts a measure to a different unit representation.

    import { days, Measure, mega, micro, miles, speedOfLight, yards } from "safe-units";
    
    const furlongs = Measure.of(220, yards, "fur");
    
    Measure.of(8, furlongs).in(miles); // "1 mi"
    Measure.of(1, miles).in(furlongs); // "8 fur"
    
    const fortnights = Measure.of(14, days, "ftn");
    const megafurlong = mega(furlongs);
    const microfortnight = micro(fortnights);
    const mfPerUFtn = megafurlong.per(microfortnight).withSymbol("Mfur/µftn");
    
    speedOfLight.in(mfPerUFtn); // "1.8026174997852542 Mfur/µftn"