uom-ts
Units of measure type safety, with no runtime overhead, supporting multiplication and division!
Why?
Because we want to be sure that we did correct operations or that we passed value with correct unit as a parameter.
In example you can by mistake put Seconds
into function that takes MilliSeconds
. In a standard scenario where you don't annotate numeric values with units, that bug would be not found until you will see that something takes 1000 times longer then it should. With annotated units TypeScript will tell you that you are puting wrong unit into function. There might be more subtle bugs, where you forget to divide by something that is a small value (in example 1.2
). Such bug will be really difficult to discover.
Big advantage of this library is that you can define your own units easly, you are not limited to units defined by author of the lib, like in many other libraries.
In example if you are creating a game you might want to define health and health rate of regeneration for your character, with this lib you can
define unit type Health = Unit<{hp: 1}>
and type HealthRegen = Rate<Health, Seconds>
, where rate is a simple alias for unit division type Rate<UNIT, OVER> = DivideUnits<UNIT, OVER>
.
;;; ; regenHealth10 as Health, 2 as HealthRegen, 1 as Seconds; // ok regenHealth10 as HealthRegen, 2 as Health, 1 as Seconds; // type error
It's also worth notice that unit types are referentialy transparent, so you can use interchangably units Unit<{hp: 1, s: -1}>
, DivideUnits<{hp: 1}, {s: 1}>
, DivideUnits<Health, Seconds>
, Rate<Health, Seconds>
, HealthRegen
and so on.
regenHealth10 as Health, 2 as Unit, 1 as Seconds; // ok regenHealth10 as Health, 2 as DivideUnits, 1 as Seconds; // ok regenHealth10 as Health, 2 as Rate, 1 as Seconds; // ok
Installation
npm install uom-ts
TypeScript version
Tested for versions from 3.0 to 4.0, but should work for all 4.x versions unless there will be backward incompatible changes like it happened in 3.x.
Examples of usage:
Creating unit and assigning it.
; ;; ; // ok ; // error; // error
Types are able to correctly multiply and divide units for you!
; ;;; ; // ok -> 2m/s; // error; // error ; // ok -> 50m; // error
Type system can warn you about invalid math, in example if you forget to divide force by mass to get acceleration.
; ; // define your own units;;;; ; // no errors; // error below because you can't add impulse to velocity; ;;applyForceforce, 20 as Seconds, body; // returns body with velocity 20.0 m/s
Operations
Math operations that you do with units must be made with functions defined in this lib or you will lose type along a way. It means that unfortunately external vector libraries won't work, but you can easly create your own (there are some examples in tests for that).
Supported operations:
- add
- sub
- mul
- div
- mod
- pow2
- sqrt2
- negate
- abs
- eq
- gt
- gte
- lt
- lte
- floor
- ceil
- round
- max
- min
- sum
All operation functions can be partially applied if you preffer more functional style. Have in mind that for operations where arguments order does matter, when you partially applies them then order of arguments is reversed. In example:
sub5 as Meters, 2 as Meters === 3 as Meters sub5 as Meters2 as Meters === -3 as Meters pipe5 as Meters, sub2 as Meters === 3 as Meters
Functions that takes array of values like max
, min
and sum
, takes only non empty arrays, but since TypeScript doesn't know if array is empty or not, you have to check it with guard function isArrayNonEmpty
(which is included in lib) before passing it.
Regulations
-
Units are created by specifing unit symbol and its exponent.
; // second^1 (time).; // meters^1/seconds^2 (acceleration); // 1/second^1 (frequency) -
When you initiliaze some const as an unit, you have to assert it against that unit so type system knows it's that unit. i.e.
const speed = 10.0 as MetersPerSecond;
. Some people don't want to use assertions "freely" in their codebase, but we can hide that in some factory functions, i.e.:const metersPerSecond = (val: number): MetersPerSecond => val as MetersPerSecond;
. -
You cannot assign zero exponent when creating new unit, because it's redundant, the same effect is when you just don't define such exponent at all.
-
For now only exponents in range <-6, 6> (integers without zero) are supported. That means that you can in example multiply cubic meters by cubic meters, which will be m^6, but you cannot multiply cubic meters by square meters by square meters, because it will be m^7 and over the range.
-
When you create new physcial units, try to use units from SI system whenever you can. In example Newtons are (kg * m)/s^2, so don't create unit
{N: 1}
, instead create{m: 1, kg: 1, s: -2}
. This way units are interchangable. If you don't do that, then you will have to create functions for explicit convertions. It's also important if you want to work with other libraries that use uom-ts, because if they will use 's' for seconds, and you will use something different, i.e. 'S', then your units won't match. -
Units that are just different scales of basic unit must be created separately for now. In example meters and inches, or seconds and milliseconds. You should create convertion functions for them.
;;;;;; // ok -
If you create generic functions operating on units, in example:
const scaleVector = <T extends AnyUnit, S extends AnyUnit>(scale: S, vec: Vec<T>) => [mul(vec[0], scale), mul(vec[1], scale)];
then to annotate return type you cannot foresight what unit type will be returned. For that case you can use operation types, here return type would beVec<MultipyUnits<S, T>>
. There is alsoDivideUnits
type andSqrtUnit
type. There are some cases where you have to use these types even that you might think you don't have to. Think about such functionconst vecLength = <T extends AnyUnit>(v: Vec<T>): T => sqrt2(add(pow2(v[0]), pow2(v[1])))
- simple pythagorean theorem. We know that length of vector of unit T will be of unit T. Unfortunately TS can't resolve that, so you have to annotate return type composed of operations made in that function, in this case it will beSqrtUnit<MultiplyUnits<T, T>>
which mathematicaly is just aT
.