Generic Measures

The default Measure class is specifically designed around using measures where the underlying values are represented by the JavaScript number type. However, it may be desirable to want measures where the values have different numeric types. For example, one might want measures with arbitrary precision or measures on the rational numbers. Creating such a measure class can be accomplished by using the createMeasureType function.

Example

Suppose we have our own number type:

class WrappedNumber {
    constructor(public readonly value: number) {}

    declare foo: () => WrappedNumber;
    // ...
}

function wrap(value: number) {
    return new WrappedNumber(value);
}

Now we're going to create our own measure type that operates on WrappedNumber types, let's call it WrappedMeasure:

import { createMeasureType, GenericMeasure, Unit } from "safe-units";

type WrappedMeasure<B, U extends Unit<B>> = GenericMeasure<WrappedNumber, B, U>;
const WrappedMeasure = createMeasureType<WrappedNumber>({
    one: () => wrap(1),
    neg: x => wrap(-x.value),
    add: (x, y) => wrap(x.value + y.value),
    sub: (x, y) => wrap(x.value - y.value),
    mult: (x, y) => wrap(x.value * y.value),
    div: (x, y) => wrap(x.value / y.value),
    reciprocal: x => wrap(1 / x.value),
    compare: (x, y) => x.value - y.value,
    format: x => `${x}`,
});

We can then use this class just as we would use Measure, except anywhere we'd expect a number we now expect a WrappedNumber.

Breakdown

Let's deconstruct this example to explain what's going on. First we start with this type definition:

type WrappedMeasure<B, U extends Unit<B>> = GenericMeasure<WrappedNumber, B, U>;

This line isn't strictly necessary, but it is often useful to have our WrappedMeasure available as a type. Having a type for the measure is useful for writing generic functions on wrapped measures. All this line does is bind the numeric type of GenericMeasure. Similarly, the Measure type has the following definition:

type Measure<B, U extends Unit<B>> = GenericMeasure<number, B, U>;

After we've defined the type of WrappedMeasure we now define the class itself by calling createMeasureType. This function takes an object which let's the generic measure type know how to perform operations on our numeric type. Note that for this simple example, we generally just unwrap the value, perform the arithmetic operation and then wrap it back up. Most of these operations should be self-explanatory, however some require some further explanation:

  • one: A function with no arguments that simply returns the 1 value or multiplicative identity of your number system. This is used to construct base units whose values are implicitly one.
  • compare: A function that returns a negative number if its first argument is less than its second, a positive number if its first argument is greater than its second, and 0 if the arguments are equal.

Usage

The returned WrappedMeasure now behaves just like Measure does except in the domain of wrapped numbers. This means we can call WrappedMeasure.of or WrappedMeasure.dimensions just as expected.

Static Methods

By default, generic measures come with a set of static methods that can be applied to measures of all numeric types. However, certain static methods may only make sense for a given numeric type. For example the Measure.trunc method exists because Math.trunc applies to number types. To add static methods to a generic measure type, simple pass an object as a second argument to createMeasureType:

declare function foo(value: WrappedNumber): WrappedNumber;
declare const mass: Mass<WrappedNumber>;

const WrappedMeasure = createMeasureType(numericOperations, {
    foo: wrapUnaryFn(foo),
});

WrappedMeasure.foo(mass);
Safe Units is developed by Jonah Scheinerman. Please contact me if you have questions or concerns.
Safe Units is distributed under the MIT open source license.

Copyright © 2024 by Jonah Scheinerman