The Measure
class provides the core of the functionality for Safe Units. A Measure
is a value associated with a unit (e.g. 5 meters, 10 seconds) in a given unit system (e.g. SI units). A Measure
may be given a symbol to indicate that it itself represents some unit. For example, 0.3048 meters is one foot and therefore might be given the symbol ft
. Measures are immutable and any operation on a measure returns a new measure.
The Measure class takes two type parameters B
and U
. The B
type parameter is the basis for the unit system being used by the measure. The U
type parameter is the type of the unit that the measure represents.
Note: The Measure
class provides functionality for manipulating units with numeric values represented by the JavaScript number
type. For different numeric types see Generic Measures.
The following functions are static functions on the Measure class used to construct or manipulate measures.
Measure.of
Measure.of<B, U>(
value: number,
quantity: Measure<B, U>,
symbol?: string,
): Measure<B, U>
Creates a measure that is a scalar multiple of a given measure. An optional symbol may be provided to name the resulting measure.
Examples:
const d1 = Measure.of(30, meters);
const feet = Measure.of(0.3048, meters, "ft");
const d2 = Measure.of(10, feet);
const minutes = Measure.of(60, seconds, "min");
Measure.dimensionless
Measure.dimensionless<B>(
unitSystem: UnitSystem<B>,
value: number,
): Measure<B, DimensionlessUnit<B>>
Creates a dimensionless measure value in the given unit system.
Examples:
const scalar = Measure.dimensionless(SIUnitSystem, 2);
const distance = Measure.of(20, meters);
const doubled = distance.times(scalar); // 40 m
Measure.dimension
Measure.dimension<B, D extends keyof B>(
unitSystem: UnitSystem<B>,
dimension: D,
symbol?: string,
): Measure<...>
Constructs a measure which represents a single dimension of the given unit system. The resulting measure is 1 of the base unit for that dimension. For more information see Defining Quantities.
const meter = Measure.dimension(SIUnitSystem, "length");
console.log(meter); // 1 m
const second = Measure.dimension(SIUnitSystem, "time");
console.log(second); // 1 s
Measure.isMeasure
Measure.isMeasure(value: any): value is Measure<any, any>
Since Measure
isn't technically a class, you can't check that a value is a measure by using instanceof
. Instead, use this method to determine if a given value is a Measure
.
Measure.prefix
Measure.prefix(prefix: string, multiplier: number): PrefixFn
Creates a function which, when given a measure, applies a prefix to that measure's symbol and multiplies it by a given dimensionless value.
Examples:
const kilo = Measure.prefix("k", 1000);
const kilometers = kilo(meters); // 1000 m
const distance = Measure.of(20, kilometers); // 20000 m
distance.in(kilometers); // 20 km
Measure<B, U>.negate(): Measure<B, U>
Returns a new measure containing the negative value of the original.
Examples:
const positive = Measure.of(30, meters);
const negative = positive.negate(); // -30 m
// Instance method
Measure<B, U>.plus(other: Measure<B, U>): Measure<B, U>
// Static functions
Measure.add<B, U>(left: Measure<B, U>, right: Measure<B, U>): Measure<B, U>
Measure.sum<B, U>(first: Measure<B, U>, ...rest: Array<Measure<B, U>>): Measure<B, U>
Measures may only be added if they have the same unit. This will produce a new measure with the same unit. The Measure.add
static function is an alias for a.plus(b)
. The Measure.sum
method must be given a list of one or more units, since we can't infer the unit for an empty list.
Examples:
const d1 = Measure.of(30, meters);
const d2 = Measure.of(10, meters);
const d3 = Measure.of(100, feet);
const t1 = Measure.of(2, minutes);
const sum1 = d1.plus(d2); // 40 m
const sum2 = Measure.add(d1, d2); // 40 m
const sum3 = Measure.sum(d1, d2, d3); // 140m
const good = d1.plus(d3); // Fine because both are lengths
// @ts-expect-error Cannot add measures representing distances and times together
const bad = d1.plus(t1);
// Instance method
Measure<B, U>.minus(other: Measure<B, U>): Measure<B, U>
// Static function
Measure.subtract<B, U>(left: Measure<B, U>, right: Measure<B, U>): Measure<U>
Measures may only be subtracted if they have the same unit. This will produce a new measure with the same unit. All of these functions behave the same.
const d1 = Measure.of(30, meters);
const d2 = Measure.of(10, meters);
const t1 = Measure.of(2, minutes);
const diff1 = d1.minus(d2); // -20 m
const diff2 = Measure.subtract(d2, d1); // 20 m
// @ts-expect-error Cannot subtract a distance from a time unit
const bad = t1.minus(d1);
// Instance method
Measure<B, U>.times<V>(other: Measure<B, V>): Measure<B, MultiplyUnits<B, U, V>>
// Static function
Measure.multiply<B, U, V>(
left: Measure<B, U>,
right: Measure<B, V>,
): Measure<B, MultiplyUnits<B, U, V>>
Multiplies two measures together and returns a new measure. The resulting unit is computed at compile time to be the result of multiplying the arguments' units together.
const mass = Measure.of(10, kilograms);
const acceleration = Measure.of(9.8, metersPerSecondsSquared);
const time = Measure.of(10, seconds);
// Works! The result of mass times acceleration is force
const force: Force = mass.times(acceleration); // 98 N
const velocity: Velocity = Measure.multiply(acceleration, time); // 98 m/s
// @ts-expect-error A force quantity cannot be assigned to a pressure quantity
const bad: Pressure = Measure.multiply(mass, acceleration);
// Instance methods
Measure<B, U>.div<V>(other: Measure<B, V>): Measure<B, DivideUnits<B, U, V>>
Measure<B, U>.over<V>(other: Measure<B, V>): Measure<B, DivideUnits<B, U, V>>
Measure<B, U>.per<V>(other: Measure<B, V>): Measure<B, DivideUnits<B, U, V>>
// Static function
Measure.divide<B, U, V>(
left: Measure<B, U>,
right: Measure<B, V>,
): Measure<B, DivideUnits<B, U, V>>
Divides two measures together and returns a new measure. The resulting unit is computed at compile time to be the result of dividing the arguments' units together. All of these functions behave the same and are provided to make writing readable units easier.
const distance = Measure.of(30, meters);
const time = Measure.of(10, seconds);
// Works! The result of distance over time is velocity
const velocity: Velocity = distance.over(time); // 300 m*s
const acceleration: Acceleration = velocity.div(time); // 30 m/s^2
// @ts-expect-error A velocity quantity cannot be assigned to an acceleration quantity
const bad: Acceleration = Measure.divide(distance, time);
Measure<B, U>.scale(value: number): Measure<B, U>
A convenience method for multiplying a measure by a dimensionless value.
const t = Measure.of(10, seconds);
const doubledShort = t.scale(2); // 20 s
const doubledLong = t.times(Measure.dimensionless(SIUnitSystem, 2)); // 20 s
Measure<B, U>.squared(): Measure<B, SqareUnit<B, U>>
Measure<B, U>.cubed(): Measure<B, CubeUnit<B, U>>
Convenience methods to square and cube a measure respectively. These are equivalent to multiplying a unit by itself the corresponding number of times.
Examples:
const side = Measure.of(10, meters);
const area: Area = side.squared(); // 100 m^2
const volume: Volume = side.cubed(); // 1000 m^3
Measure<B, U>.reciprocal(): Measure<B, ReciprocalUnit<B, U>>
Measure<B, U>.inverse(): Measure<B, ReciprocalUnit<B, U>>
Computes the reciprocal of the value and unit of the measure. Both methods are identical.
Examples:
const freq: Frequency = Measure.of(30, hertz); // 30 1/s
const cycle: Time = freq.inverse(); // 1/30 s
The Measure
class contains a number of static methods from the JavaScript Math
object wrapped to work on measures. Many of these functions take in a single measure and return a single measure without changing its unit:
Measure.abs
Measure.ceil
Measure.floor
Measure.fround
Measure.round
Measure.trunc
There is also a wrapper for Math.hypot
that takes in one or more measures all with the same unit and returns a single measure of that unit.
Examples:
const distance = Measure.of(-9.8, meters);
Measure.abs(distance); // 9.8 m
Measure.trunc(distance); // -9 m
const width = Measure.of(3, meters);
const height = Measure.of(4, meters);
Measure.hypot(width, height); // 5 m
Measure<B, U>.lt(other: Measure<B, U>): boolean;
Measure<B, U>.lte(other: Measure<B, U>): boolean;
Measure<B, U>.eq(other: Measure<B, U>): boolean;
Measure<B, U>.neq(other: Measure<B, U>): boolean;
Measure<B, U>.gte(other: Measure<B, U>): boolean;
Measure<B, U>.gt(other: Measure<B, U>): boolean;
Measures are only comparable if they are of the same unit. Attempting to compare measures with different units is a compile time error.
Examples:
const t1 = Measure.of(30, minutes);
const t2 = Measure.of(0.25, hours);
const d1 = Measure.of(10, meters);
t1.gt(t2); // true
// @ts-expect-error Cannot compare time and distance values
t1.eq(d1);
Measure<B, U>.withSymbol(symbol: string): Measure<B, U>;
Duplicates the current measure and gives the new measure a symbol. Symbols are specific to an instance of a measure, performing operations on that measure will not forward along any symbols to the resulting measures. Calling measure.withSymbol(symbol)
is equivalent to calling Measure.of(1, measure, symbol)
. The symbol of a measure can be seen by accessing the readonly symbol
field.
const squareMeters = meters.squared().withSymbol("sq. m");
squareMeters.symbol; // "sq. m"
// All of the following lose the symbol from squareMeters:
const r1 = squareMeters.scale(2);
const r2 = Measure.of(10, squareMeters);
const r3 = Measure.divide(squareMeters, meters);
Symbols are used in converting other measures into strings as can be seen below.
Measure<B, U>.toString(formatter?: MeasureFormatter): string;
Returns a string version of the unit, ignoring any symbol information on that measure. This method optionally takes a formatter which has the following interface:
interface MeasureFormatter {
formatValue?: (value: number) => string;
formatUnit?: (unit: Unit, unitSystem: UnitSystem) => string;
}
The formatValue
function, if provided, will be applied to customize the formatting of the numeric value of the measure in the resulting string. The formatUnit
, if provided, will be passed the unit and unit system of the measure in order to customize how that is formatted. When calling the Measure.in
method, the formatUnit
function will only be used if the unit being used to express the measure has no symbol.
Examples:
const kilometers = Measure.of(1000, meters, "km");
// Could also be written as: Measure.of(1000, meters).withSymbol("km")
const distance = Measure.of(5, kilometers);
distance.toString(); // "5000 m"
distance.toString({
formatValue: x => x.toExponential(),
formatUnit: () => "meters",
}); // "5e+3 meters"
const force = Measure.of(30, newtons);
force.toString(); // "30 kg * m * s^-2"
force.toString({ formatValue: x => x.toPrecision(5) }); // "30.000 kg * m * s^-2"
Measure<B, U>.in(unit: Measure<B, U>, formatter?: MeasureFormatter): string;
Measure<B, U>.valueIn(unit: Measure<B, U>): number;
These methods can be used if you want to display or represent a measure in terms of another measure that represents a unit. The measures must have the same unit in order for the conversion to be valid.
The in
method will return a formatted string containing the unit information. This takes an optional formatter parameter that behaves just like the toString()
method.
The valueIn
will just return the numeric value of the conversion. Be careful when using valueIn
as this erases the type information of the unit and is no longer type safe.
Examples:
const kilometers = Measure.of(1000, meters, "km");
const distance = Measure.of(5500, meters);
distance.in(kilometers); // "5.5 km"
distance.in(kilometers, { formatValue: x => x.toPrecision(3) }); // "5.50 km"
distance.valueIn(kilometers); // 5.5
Measure<B, U>.unsafeMap(valueMap: (value: number) => number): Measure<B, U>;
Measure<B, U>.unsafeMap<V>(
valueMap: (value: number) => number,
unitMap: (unit: U) => V,
): Measure<B, V>;
If only one argument is passed, performs a mapping on the value of a measure without affecting the unit of the measure. If both arguments are passed maps both the unit and value of a measure. This is generally used for internal purposes and should be avoided whenever possible. Instead consider using a function wrapper.
A Measure
represents a value in terms its base units. For example, in the SI unit system, the base unit for length, mass, and time are meters, kilograms, and seconds respectively. We can define values units for feet, pounds, and minutes in this system but they will be represented under the hood as those base units.
const feet: Length = Measure.of(0.3048, meters, "ft");
const pounds: Mass = Measure.of(0.453592, kilograms, "lb");
const minutes: Time = Measure.of(60, seconds, "min");
If we use these units to derive new values, those resulting values will still be represented using meters, kilograms, and seconds:
const yards = Measure.of(3, feet); // 0.9144 m
const stones = Measure.of(14, pounds); // 6.35029 kg
const hours = Measure.of(60, minutes); // 3600 s
In this way, we never actually have to perform unit conversions since, under the hood, all measures with the same dimension are always represented with the same units. We can format these values using any unit we want with the in
method
It is often desirable to convert operations on numbers into operations on measures. Frequently, these functions make no change on the unit of a value. For example, suppose we want to make an absolute value function that operates on measures. We'd expect the function perserve the unit of the input. We can simply wrap an existing absolute value function using wrapUnaryFn
:
const measureAbs = wrapUnaryFn(Math.abs);
const time = Measure.of(-30, seconds);
measureAbs(time); // 30 s
The following function wrappers are provided:
wrapUnaryFn
- Wraps a function of a single number.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.