Skip to content

observerly/ts-units

 
 

Repository files navigation

Physical Units for TypeScript

CI Build npm version

@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));

Basic Usage

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.

Displaying

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>`;

Conversion

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.

Comparing

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);

Addition and Subtracting

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.

Multiplication and Division

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);

Built-in Units

Here’s a reference of all units that we currently have built in to the library.

Planar Angles

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

Solid Angles

import {SolidAngle, steradians} from '@buge/ts-units/angle/solid';
const angle: SolidAngle = steradians(1);

Units:
steradians, squareDegrees

Area

import {Area, squareMeters} from '@buge/ts-units/area';
const area: Area = squareMeters(15);

Units:
squareMeters

Electrical Capacitance

import {Capacitance, microfarads} from '@buge/ts-units/eletric/capacitance';
const capacitance: Capacitance = microfarads(4700);

Units:
farads, microfarads, nanofarads, picofarads

Electric Charge

import {Charge, coulombs} from '@buge/ts-units/eletric/charge';
const charge: Charge = coulombs(5000);

Units:
coulombs

Electrical Conductance

import {Conductance, siemens} from '@buge/ts-units/eletric/conductance';
const conductance: Conductance = siemens(0.2);

Units:
siemens

Electrical Current

import {Current, amperes} from '@buge/ts-units/eletric/current';
const current: Current = amperes(10);

Units:
amperes

Electrical Inductance

import {Inductance, henries} from '@buge/ts-units/eletric/inductance';
const inductance: Inductance = henries(1);

Units:
henries

Electrical Resistance

import {Resistance, ohms} from '@buge/ts-units/eletric/resistance';
const resistance: Resistance = ohms(560);

Units:
ohms

Electric Voltage

import {Voltage, volts} from '@buge/ts-units/eletric/voltage';
const voltage: Voltage = volts(220);

Units:
volts

Energy

import {Energy, joules} from '@buge/ts-units/energy';
const energy: Energy = joules(4.1868);

Units:
joules

Force

import {Force, newtons} from '@buge/ts-units/force';
const force: Force = newtons(608);

Units:
newtons

Frequency

import {Frequency, hertz} from '@buge/ts-units/force';
const frequency: Frequency = hertz(608);

Units:
hertz

Length

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

Luminous Flux

import {Flux, lumes} from '@buge/ts-units/luminous/flux';
const flux: Flux = lumens(800);

Units:
lumens

Illuminance

import {Illuminance, lux} from '@buge/ts-units/luminous/illuminance';
const illuminance: Illuminance = lux(35000);

Units:
lux

Luminous Intensity

import {Intensity, candelas} from '@buge/ts-units/luminous/intensity';
const intensity: Intensity = candelas(135);

Units:
candelas

Magnetic Flux

import {Flux, webers} from '@buge/ts-units/magnetic/flux';
const flux: Flux = webers(800);

Units:
webers

Mass

import {Mass, kilograms} from '@buge/ts-units/mass';
const mass: Mass = kilograms(5);

Units:
kilograms, grams

Scalar

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

Speed

import {Speed, metersPerSecond} from '@buge/ts-units/speed';
const speed: Speed = metersPerSecond(343);

Units:
metersPerSecond, kilometersPerHour, milesPerHour, knots, feetPerSecond

Power

import {Power, watts} from '@buge/ts-units/power';
const power: Power = watts(800);

Units:
watt

Pressure

import {Pressure, pascals} from '@buge/ts-units/pressure';
const pressure: Pressure = pascals(101325);

Units:
pascals

Radioactivity

import {Radioactivity, becquerels} from '@buge/ts-units/radioactive/decay';
const radioactivity: Radioactivity = becquerels(20);

Units:
becquerels

Absorbed and Equivaelnt Doses of Ionizing Radiation

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

Temperature

import {Temperature, celsius} from '@buge/ts-units/temperature';
const temperature: Temperature = celsius(23.1);

Units:
kelvin, celsius, fahrenheit, rankine

Time

import {Time, minutes} from '@buge/ts-units/time';
const time: Time = minutes(5);

Units:
seconds, milliseconds (msec), microseconds (usec), nanoseconds, minutes, hours

Volume

import {Volume, cubicMeters} from '@buge/ts-units/volume';
const volume: Volume = cubicMeters(3);

Units:
cubicMeters

Defining new Units

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.

Defining new Dimensions

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);

Defining new Arithmetic

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);

Limitations

Lack of Kinds

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};

Lack of input flexibility

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

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 94.2%
  • JavaScript 5.8%