@buge/ts-units
is a package for modeling typed physical units in TypeScript.
It allows you to tell users what you need in a typesafe manner
function setTemperature(t: Temperature) {
...
}
while letting them work in units that make sense to them
setTemperature(celsius(21.3));
Units can easily be converted from one to another
const t = celsius(21.3).in(fahrenheit);
compared
meters(1) < feet(4);
and used in mathematical expressions
const length = meters(5).plus(centimeters(3));
const speed = length.per(seconds(2));
Install from npm or using your favorite package manager:
npm install @buge/ts-units
Simply declare quantities using the built-in units:
import {Length, meters} from '@buge/ts-units/length';
const length: Length = meters(5);
See Built-in Units below for an overview of all built-in units.
Quantities provide a toString()
method that allows them to be easily rendered
to a string:
celsius(23.1).toString(); // '23.1ºC'
For custom formatting, the amount and symbol can be easily extracted from the quantity:
const temp = celsius(23.1);
const html =
`<span class="amount">${temp.amount.toFixed(1)}</span>` +
`<span class="symbol">${temp.unit.symbol}</span>`;
Quantities can easily be converted from one unit to another using the in
method:
const length = meters(5.3).in(feet);
const temp = celsius(23.1).in(fahrenheit);
Quantities can (perhaps obviously) only be converted from units belonging to the same physical dimension in this way, but see Multiplication and Division below for some more advanced conversions.
Quantities can be freely compared to one another using standard TypeScript
operators (thanks to valueOf
):
meters(1) < feet(4);
Because of the inprecision in floating point arithmetics, you should prefer to
use the isCloseTo
method when checking a quantity for equality:
// Returns whether 1m is within 0.01 feet of 3.28 feet.
meters(1).isCloseTo(feet(3.28), 0.01);
Quantities of the same physical dimension can be freely added using plus
and
subtracted using minus
:
const time = minutes(3).plus(seconds(2));
const length = meters(5).minus(feet(1));
The unit of the quantities returned will default to the unit of the quantity being added or subtracted.
Quantities can be multiplied with times
and divided with per
, resulting in
new dimensions being returned:
const area: Area = meters(5).times(meters(6));
const speed: Speed = kilometers(150).per(hours(2));
A special case are multiplications with dimensionless units which result in the same unit being returned:
const length: Length = meters(5).times(percent(10)); // 0.5m
You can square and cube quantities and take their reciprocal:
const area: Area = meters(3).squared();
const volume: Volume = meters(2).cubed();
const frequency: Frequency = seconds(3).reciprocal();
You can use the relationship between quantities to define unit conversions:
const riceDensity = grams(220).per(cups);
const iNeed = grams(500).per(riceDensity).in(ounces);
Here’s a reference of all units that we currently have built in to the library.
import {Angle, degrees, sin} from '@buge/ts-units/angle';
const angle: Angle = degrees(30);
const s = sin(angle);
Units:
radians, degrees, turns
Trigonometric Functions:
sin, cos, tan, asin, acos, atan, atan2
import {SolidAngle, steradians} from '@buge/ts-units/angle/solid';
const angle: SolidAngle = steradians(1);
Units:
steradians, squareDegrees
import {Area, squareMeters} from '@buge/ts-units/area';
const area: Area = squareMeters(15);
Units:
squareMeters
import {Capacitance, microfarads} from '@buge/ts-units/eletric/capacitance';
const capacitance: Capacitance = microfarads(4700);
Units:
farads, microfarads, nanofarads, picofarads
import {Charge, coulombs} from '@buge/ts-units/eletric/charge';
const charge: Charge = coulombs(5000);
Units:
coulombs
import {Conductance, siemens} from '@buge/ts-units/eletric/conductance';
const conductance: Conductance = siemens(0.2);
Units:
siemens
import {Current, amperes} from '@buge/ts-units/eletric/current';
const current: Current = amperes(10);
Units:
amperes
import {Inductance, henries} from '@buge/ts-units/eletric/inductance';
const inductance: Inductance = henries(1);
Units:
henries
import {Resistance, ohms} from '@buge/ts-units/eletric/resistance';
const resistance: Resistance = ohms(560);
Units:
ohms
import {Voltage, volts} from '@buge/ts-units/eletric/voltage';
const voltage: Voltage = volts(220);
Units:
volts
import {Energy, joules} from '@buge/ts-units/energy';
const energy: Energy = joules(4.1868);
Units:
joules
import {Force, newtons} from '@buge/ts-units/force';
const force: Force = newtons(608);
Units:
newtons
import {Frequency, hertz} from '@buge/ts-units/force';
const frequency: Frequency = hertz(608);
Units:
hertz
import {Length, meters} from '@buge/ts-units/length';
const length: Length = meters(5);
Metric Units:
meters, kilometers, centimeters, millimeters, micrometers, nanometers,
picometers, femtometers, fermi, angstroms, microns
Imperial Units:
yards, feet, inches, chains, furlongs, miles
Marine Units:
fathoms, nauticalMiles
Astronomical Units:
astronomicalUnits
import {Flux, lumes} from '@buge/ts-units/luminous/flux';
const flux: Flux = lumens(800);
Units:
lumens
import {Illuminance, lux} from '@buge/ts-units/luminous/illuminance';
const illuminance: Illuminance = lux(35000);
Units:
lux
import {Intensity, candelas} from '@buge/ts-units/luminous/intensity';
const intensity: Intensity = candelas(135);
Units:
candelas
import {Flux, webers} from '@buge/ts-units/magnetic/flux';
const flux: Flux = webers(800);
Units:
webers
import {Mass, kilograms} from '@buge/ts-units/mass';
const mass: Mass = kilograms(5);
Units:
kilograms, grams
Technically not a physical dimension but still very useful.
import {Scalar, percent} from '@buge/ts-units/scalar';
const scalar: Scalar = percent(20);
Units:
percent, permille, permyriad
import {Speed, metersPerSecond} from '@buge/ts-units/speed';
const speed: Speed = metersPerSecond(343);
Units:
metersPerSecond, kilometersPerHour, milesPerHour, knots, feetPerSecond
import {Power, watts} from '@buge/ts-units/power';
const power: Power = watts(800);
Units:
watt
import {Pressure, pascals} from '@buge/ts-units/pressure';
const pressure: Pressure = pascals(101325);
Units:
pascals
import {Radioactivity, becquerels} from '@buge/ts-units/radioactive/decay';
const radioactivity: Radioactivity = becquerels(20);
Units:
becquerels
import {Dose, grays, sieverts} from '@buge/ts-units/radioactive/dose';
const absorbed: Dose = grays(20e-6);
const equivaelnt: Dose = sieverts(1.5e-3);
Units:
grays, sieverts
import {Temperature, celsius} from '@buge/ts-units/temperature';
const temperature: Temperature = celsius(23.1);
Units:
kelvin, celsius, fahrenheit, rankine
import {Time, minutes} from '@buge/ts-units/time';
const time: Time = minutes(5);
Units:
seconds, milliseconds (msec), microseconds (usec), nanoseconds, minutes, hours
import {Volume, cubicMeters} from '@buge/ts-units/volume';
const volume: Volume = cubicMeters(3);
Units:
cubicMeters
New units of existing dimensions can easily be defined on the basis of an existing one. As an example, here’s how the yard is defined:
const yards: Unit<Length> = meters.times(0.9144).withSymbol('yd');
Similar how quantities can be multiplied and divided you can create new derived
units by multiplying them using time
, dividing them using per
, squaring
them using squared
, cubing using cubed
and taking their reciprocal using
reciprocal
.
const knots: Unit<Speed> = nauticalMiles.per(hours).withSymbol('kn');
const mps2: Unit<Acceleration> = meters
.per(seconds.squared())
.withSymbol('m/s²');
As shown above, you can configure the symbol for the unit using withSymbol
that will be suffixed to the amount when printing a quantity using
toString()
. You can provide any arbitrary string. The symbol can be retrieved
from the unit using the .symbol
property or from a quantity using
.unit.symbol
.
We provide a dedicated method to create derived units with SI prefix that both scale the unit and prepend the prefix to the symbol.
const micrograms = gram.withSiPrefix('μ');
micrograms(2.13).toString(); // 2.13μg
The supported SI prefixes are enumerated in unit.SiPrefix
.
We provide units and dimensions for all SI base units and for many derived units. These can be used as building blocks for defining many other units.
Sometimes, you’ll find yourself wanting to define units for a new substance or “thing” though.
Measurable things are defined through “dimensions”. In scientific literature a
dimension is often denoted with capital letters and square brackets. For
example, the dimension of length is often denoted as [L]
, that of
area as [L]2
and that of speed as
[L][T]-1
.
We ensure type safety by encoding the dimensions of units and quantities as TypeScript Literal Types. For example, the dimension mentioned above are defined as:
type Length = {length: 1};
type Area = {length: 2};
type Speed = {length: 1; time: -1};
To define a new dimension, start by defining a literal type for it. You’ll also need to define a constant for it that can be used at runtime, since TypeScript types all disappear after compilation. For example, if you would like to introduce quantities of money you might write:
// In money/dimension.ts
export type Money = {money: 1};
export const Money: Money = {money: 1};
We use a convention that we use the same capitalization for the type and for
the constant value since it allows us (in most cases) to just forget about
whether we are using the type or the value. We also store the dimension in a
dimension.ts
alongside the units so that we can distinguish Length
as a
dimension vs. a Length
as an amount of a quantity.
Next, you will want to define a base unit for the dimension. Let’s take the US dollar as the base unit:
// In money/units.ts
import * as dimension from './dimension';
export type Money = Quantity<number, dimension.Money>;
export const dollars: Unit<number, dimension.Money> = makeUnit('$', dimension.Money);
You can now use your new dimension and unit to define quantities:
const pleaseDonateToACharityOfYourChoice: Money = usd(10);
The default units provided by the library are all based on the native representation of JavaScript number which could lead to a precision error. The most infamous example being (0.1 + 0.2).
The library provides a way to avoid this problem by giving you the possibility to implement your own arithmetic.
Each group of units exposes a function withValueType
that returns the units using your own arithmetic.
Concretely, you can use other external library that provides more robust representation of numbers. For instance decimal.js:
import {Arithmetic, Geometric} from "@buge/ts-units";
import {Decimal } from 'decimal.js';
import {withValueType as temperatureWithValueType} from '@buge/ts-units/temperature';
import {withValueType as angleWithValueType} from '@buge/ts-units/angle';
const DecimalArithmetic: Arithmetic<Decimal> = {
fromNative(value: number): Decimal {
return new Decimal(value);
},
toNative(value: Decimal): number {
return value.toNumber();
}
add(left: Decimal, right: Decimal): Decimal {
return left.add(right);
},
sub(left: Decimal, right: Decimal): Decimal {
return left.sub(right);
},
mul(left: Decimal, right: Decimal): Decimal {
return left.mul(right);
},
div(left: Decimal, right: Decimal): Decimal {
return left.div(right);
},
pow(base: Decimal, exponent: Decimal): Decimal {
return base.pow(exponent);
},
abs(value: Decimal): Decimal {
return value.abs();
},
compare(left: Decimal, right: Decimal): number {
return left.comparedTo(right);
}
};
const {celsius} = temperatureWithValueType(DecimalArithmetic);
// Planar Angles needs also an implementation of geometric.
const DecimalGeometric: Geometric<Decimal> = {
sin(value: Decimal): Decimal {
return value.sin();
},
cos(value: Decimal): Decimal {
return value.cos();
},
tan(value: Decimal): Decimal {
return value.tan();
},
asin(value: Decimal): Decimal {
return value.asin();
},
acos(value: Decimal): Decimal {
return value.acos();
},
atan(value: Decimal): Decimal {
return value.atan();
},
atan2(left: Decimal, right: Decimal): Decimal {
return Decimal.atan2(left, right);
}
};
const {radians, sin, cos} = angleWithValueType(DecimalArithmetic, DecimalGeometric);
Finally, if you want to create your units from scratch using your own arithmetic, you can use the function makeUnitFactory
.
import * as dimension from './dimension';
import {DecimalArithmetic} from './arithmetic'
const {makeUnit} = makeUnitFactory(DecimalArithmetic);
export type Money = Quantity<Decimal, dimension.Money>;
export const dollars: Unit<Decimal, dimension.Money> = makeUnit('$', dimension.Money);
Two types are compatible if they share the same dimensions. That is, they can be reduced to the same base unit equivalents. This can mean that some things may be compatible when the actually should not be:
setFrequency(hertz(100));
setFrequency(becquerels(100));
radiationAlarm(grays(2));
radiationAlarm(sieverts(2));
ISO 80000-1:2009(E) defines these as separate “kinds” but we currently provide no way to check for the kind of a quantity at compile time or runtime.
In some cases we have taken the liberty to model the kind of a quantity as a
different unit, specifically with planar angles and solid angles. While these
are strictly a dimensionless quantity of [L]/[L]
and [L]^2/[L]^2
respectively, we model these as:
type Angle = {angle: 1};
type SolidAngle = {angle: 2};
Currently when declaring quantities you can only use native number as input.
import {meters} from '@buge/ts-units/length';
// It won't work
const length = meters('5');
import {withValueType} from '@buge/ts-units/length';
const { meters } = withValueType(DecimalArithmetic);
// It won't work
const length = meters(new Decimal(5));
Moreover, the arithmetic function of Unit
and Quantity
also support only native number.
It could be improve in future