Open dsherret opened 10 years ago
quite interesting, we haven't gotten that far, our arithetic is very simple
I have updated it a bit to be more robust and easy to use and published it as an open source lib. I hope you will like it!
Here's a link: https://github.com/mindbrave/uom-ts
I have implemented typelevel arithmetic on (Peano-encoded) natural numbers here: https://github.com/atennapel/ts-typelevel-computation/blob/master/src/Nat.ts, maybe it's useful for units of measure. It includes comparisons, addition, subtraction, multiplication, division, mod, pow, sqrt, log2 and gcd.
I'm working on a browser game with a canvas and different distance units like CanvasClientSpace (browser pixels), CanvasMemorySpace (memory space of the canvas, 2x browser pixels on 4k screens), GameSpace (position unit for drawing objects to the canvas). Using the correct values for the algorithms like collision detection etc. drive me nuts, so I came here and I really like @fenduru 's comment on unique symbol pseudotypes so I do this now (used a more general example):
// lib/unit.ts
declare const UnitSymbol: unique symbol
export type Unit<S> = number & {[UnitSymbol]: S}
// src/code.ts
declare const MeterSymbol: unique symbol
declare const SquareMetersSymbol: unique symbol
declare const SecondsSymbol: unique symbol
type Meters = Unit<typeof MeterSymbol>
type SquareMeters = Unit<typeof SquareMetersSymbol>
type Seconds = Unit<typeof SecondsSymbol>
function area(a: Meters, b: Meters): SquareMeters {
return (a * b) as SquareMeters
}
const twoMeters = 2 as Meters
const fiveSeconds = 5 as Seconds
area(2, 4); // err
area(twoMeters, 4) // err
area(twoMeters, fiveSeconds); // err
area(twoMeters, twoMeters) // ok
I'd also like to show another example where we can use pseudo-primitive types for more secure code
// lib/strict.ts
declare const StrictSymbol: unique symbol
export type Strict<T, S> = T & {[StrictSymbol]: S}
// src/code.ts
declare const IBANSymbol: unique symbol
type IBAN = Strict<string, typeof IBANSymbol>
const INVALID_IBAN = Symbol()
const unsafeIBAN = 'DE00-0000-0000-0000-0000-00' // from user input
function validateIBAN(iban: string): IBAN | typeof INVALID_IBAN {
// validate: returns INVALID_IBAN if invalid
return iban as IBAN
}
async function createAccount(iban: IBAN) {}
{ // nice try
createAccount('bla') // err
}
{ // meep: might be invalid
const iban = validateIBAN(unsafeIBAN)
createAccount(iban) // err
}
{
const iban = validateIBAN(unsafeIBAN)
if (iban !== INVALID_IBAN) {
createAccount(iban)
}
}
Of course you need to write some boilerplate code and the typesystem has no idea about units so you have to create every type for every unit combination you want to support. On the other hand, here is an example what you can do, if this will not land.
Here is my user-space implementation of this: Playground.
EDIT: Upon closer reading of the discussion above, I realize that an important criteria is not to incur runtime overhead. My library below definitely incurs runtime overhead as the operators are defined as function calls on a wrapper object. For obvious reasons, they are also not as succint as built-in support for such operators.
I didn't see this feature request until just now but I wanted to let folks know that I ended up implementing a physical units library over the course of the past year that does many of the things being discussed above. I did this mostly for my own entertainment, trying to learn more about advanced TypeScript types and because I needed some type safety in a personal home automation project that I've been working on:
https://github.com/buge/ts-units
Coincidentally, I did something similar to what mindbrave was suggesting above but with indexed access types. Here's an example of what adding two exponents looks like:
export type Add<A extends Exponent, B extends Exponent> =
_Add[UndefinedToZero<A>][UndefinedToZero<B>];
interface _Add extends BinaryTable {
// More numbers here
[2]: {
[-6]: -4;
[-5]: -3;
[-4]: -2;
[-3]: -1;
[-2]: undefined;
[-1]: 1;
[0]: 2;
[1]: 3;
[2]: 4;
[3]: 5;
[4]: 6;
[5]: never;
[6]: never;
};
// More numbers here
};
Only exponents up to 6 are supported right now, but this is easily extensible as those tables are generated by a script.
This allows you to freely create new units or quantities from existing ones:
// All of these units are built in, but showing here for illustration:
type Speed = {length: 1; time: -1};
const metersPerSecond: Unit<Speed> = meters.per(seconds);
const speed: Quantity<Speed> = meters(10).per(seconds(2));
assert(speed == metersPerSecond(5));
I've currently implemented all SI base and named derived units as well as some imperial length units. I'm planning to add more units (e.g. US volumetric ones) over the coming weeks / months or as people help contribute them.
I expanded the solution proposed by DaAitch to handle any primitive type.
declare const TagTypeSymbol:unique symbol
export type TagType<P, S> = P&{ [TagTypeSymbol]:S }
declare const MeterSymbol: unique symbol
declare const SquareMetersSymbol: unique symbol
declare const SecondsSymbol: unique symbol
declare const VelocityTxtSymbol: unique symbol
type Meters = TagType<number, typeof MeterSymbol>
type SquareMeters = TagType<number, typeof SquareMetersSymbol>
type Seconds = TagType<number, typeof SecondsSymbol>
type VelocityTxt = TagType<string, typeof VelocityTxtSymbol>
It would be great if something like this was baked into Typescript. Perhaps something using the unique
(or another) keyword:
type Meters = unique number
type SquareMeters = unique number
type Seconds = unique number
type VelocityTxt = unique string
What would be even better is to support union/intersection unique types:
type Norm1 = unique number
type Weight = unique number
type WeightNorm1 = Weight & Norm1
This is a bit long for a comment, but... it's relevant. I added a unit inference engine to the Boo language in 2006 and literally everyone ignored it, but I still think the basic idea was a good one. Some of my main conclusions:
It's tempting to make units a subtype of number
, e.g. number<kg>
where number
itself means "dimensionless number" or something. But this kind of design doesn't support other kinds of numbers, like complex numbers or BigInt
s. So it is better if values of any type can have units attached to them (or allow "opt in" of types to units).
(This argument makes more sense in languages with operator overloading though, so that units can Just Work on all number types. I will describe a design based on the concept of units as "mostly independent" of types, but whether that's the Right approach is debatable.)
Also, when implementing a unit system it's very useful if the system can support concepts that are similar to, but distinct from, units. For example, "tainted" types (e.g. validated vs unvalidated values), tagged types in general (see #4895), absolute axes (e.g. emitting an error for vector.x = point.x
because point.x
is an absolute location while vector.x
is relative, or for p1.x = p2.y
because there's an axis mismatch), or other knowledge ("number is between 1 and 100"). For this reason I suspect that the whole idea of having "a type" for values is misguided; multiple parallel type systems can exist at once. I could say more but I'll just say it's worth exploring whether, after some preprocessing, it's possible to run the unit system in an independent thread, parallel to the normal type checker.
A unit checking system generally requires unit annotations. A unit inference system has less need of annotations. For example, consider
function formula(x: number, y: number, z: number) { return x * x + y * z + z; }
With no annotations, the units on x
, y
and z
are unknown, so a unit checker can't do much. Maybe there's an any
unit type and the function's unit type defaults to (any,any,any): any
, or maybe the units are all assumed to be '1', meaning dimensionless, so ('1','1','1'): '1'
(if we consider unit strings as lists of units, the empty string logically also means "dimensionless": ('','',''): ''
). Or maybe the unit checker assumes x
, y
and z
have three different polymorphic types .x .y .z
, but decides they are incompatible so you're not allowed to do x * x + y * z + z
.
But in a unit-inference system, x y z
automatically get implicit units that are unknown, but named (IOW type variables); let's call them .x .y .z
. Then:
x * x
has units .x * .x
, or .x^2
if we use ^
for powersy * z
has units .y * .z
, or .y .z
if unit multiplication is implicit.x * .x
has units equal to .y * .z
because the +
operator requires the same unit on both sides, so we can replace one of the variables with a substitution equation. Let's arbitrarily assign .z = .x^2 / .y
..x^2
has units equal to .z
. Since .z
has a definition already, we substitute and get .x^2 == .x^2 / .y
.y = 1
, and indirectly implies .z = .x * .x
formula
has type (x: number'.x', y: number'1', z: number'.x^2') => number'.x^2'
. Parameter units should be polymorphic by default, so actually the type is <'.x'>(x: number'.x', y: number'1', z: number'.x^2') => number'.x^2'
.Let's look at a more real-world example. Suppose I need to calculate the size of a Powerpoint slide full of bullet points. I start by importing a third-party font-measurement library written like this:
// Third-party library, not designed for units support
export class Font {
readonly lineHeight: number;
readonly missingCharWidth: number;
private glyphWidths = new Map<string, number>();
static default: Font;
getTextWidth(str: string): number {
let width = 0;
for (let i = 0; i < str.length; i++) {
width += this.glyphWidths.get(str[i]) ?? this.missingCharWidth;
}
return width;
}
}
It wasn't designed for units, but let's assume optimistically that it was compiled to d.ts by a new TypeScript version with units support, or compiled from source. This way we can assume that the compiler has detected some unit relationships:
// Given a class, a mostly-safe assumption is that each member can have its own
// unit, and different instances have separate unit sets. So given
// `let x: Font, y: Font`, there are many units including `.x.lineHeight`,
// `.y.lineHeight`, `.x.missingCharWidth`, and `.y.missingCharWidth`.
export class Font {
readonly lineHeight: number;
readonly missingCharWidth: number;
private glyphWidths = new Map<string, number>();
static default: Font;
getTextWidth(str: string): number {
// Assume there's a special unit `#` for literals without units. `#` is
// normally treated as dimensionless when used with `* /`, so the unit
// of `x * 2` is `unitof typeof x`, but it's treated as "unit of the
// other thing" when used with `+ - == != > < =`, so e.g. `x + 3` and
// `x = 3` are always legal and do not narrow or widen the type of `x`.
let width = 0;
for (let i = 0; i < str.length; i++) {
// I'm thinking the compiler can treat `width` as if it has a new
// unit on each iteration of the loop so that a geometric mean
// calculation like `let m = 1; for (let x of xs) m *= x` does not
// force the conclusion that `x` and `xs` are dimensionless.
// (edit: nm, the system would have to be unrealistically capable
// to reach any other conclusion about a geometric mean function.)
//
// In this case it's necessary that `width`'s unit changes from `#`
// to the type of the expression. The expression has two parts,
// `this.glyphWidths.get(k)` (with unit `.this.glyphWidths:V`) and
// `this.missingCharWidth` (with unit `.this.missingCharWidth`). If
// the unit system does NOT support union types, the compiler can
// conclude these two units are equal since they both equal
// `.width`. If union types are supported, `??` should produce
// a union, so that the right-hand side gets a unit of
// `.this.glyphWidths:V | .this.missingCharWidth`. But because
// this is a loop, ultimately this unit will be equated to itself,
// which forces the inference engine to conclude that
// `.this.glyphWidths:V = .this.missingCharWidth`.
width += this.glyphWidths.get(str[i]) ?? this.missingCharWidth;
}
// So the return unit is .this.glyphWidths:V aka .this.missingCharWidth.
// Notably it's not method-polymorphic: it's the same for each call.
return width;
}
}
So now I write my own code with some unit annotations. I determined that for unit checking I needed up to 7 annotations, but this is unit inference and I've only added 3. Ahh, but it has a bug in it! Can the compiler spot it? Can you?
unit pixel = px; // optional unit definition
/** Word-wraps some text with a proportional-width font */
function wordWrap(text: string, maxWidth: number'px', font = Font.default) {
const lines = [], spaceWidth = font.getTextWidth(' ');
let currentLine = '', currentWidth = 0;
let width = 0;
for (const word of text.split(' ')) {
const wordWidth = font.getTextWidth(word);
if (currentWidth + wordWidth > maxWidth && currentWidth !== 0) {
lines.push(currentLine);
currentLine = '', currentWidth = 0;
}
if (currentLine) currentLine += ' ';
currentLine += word;
currentWidth += currentLine.length + spaceWidth;
width = Math.max(maxWidth, currentWidth);
}
lines.push(currentLine);
return { lines, width };
}
let bulletWidth = 15'px';
let indentSize = 12'px';
/** Word-wraps a bullet-point paragraph and returns info about the formatted paragraph */
function formatBulletPoint(nestingLevel: number, text: string, maxWidth: number, font = Font.default) {
let indent = nestingLevel * indentSize + bulletWidth;
let { lines, width } = wordWrap(text, maxWidth - indent, font);
return {
indent, lines,
height: lines.length * font.lineHeight,
width: indent + width,
};
}
The bug is that currentWidth += currentLine.length + spaceWidth
should say currentWidth += wordWidth + spaceWidth
. If we assume that currentLine.length
is dimensionless, the compiler should be able to spot the bug locally within wordWrap
:
function wordWrap(text: string, maxWidth: number'px', font = Font.default) {
const lines = []; // unit '.lines'
const spaceWidth = font.getTextWidth(' '); // unit '.Font.glyphWidths:V'
let currentLine = ''; // unit '.currentLine'
let currentWidth = 0; // unit '.currentWidth'
let width = 0; // unit '.width'
for (const word of text.split(' ')) {
// wordWidth: number'.font.glyphWidths:V'
const wordWidth = font.getTextWidth(word);
// Implies currentWidth, wordWidth, maxWidth are all the same unit.
// Since `maxWidth: number'px'`, they must all have unit 'px'. We
// can also conclude that '.font.glyphWidths:V' (and even
// '.Font.default.glyphWidths:V') are 'px', which in turn implies
// that `spaceWidth` is 'px'.
if (currentWidth + wordWidth > maxWidth && currentWidth !== 0) {
lines.push(currentLine);
currentLine = '', currentWidth = 0;
}
if (currentLine) currentLine += ' ';
currentLine += word;
// If array lengths are dimensionless by default, implying
// `.spaceWidth = 1`. An error is detected here, because this conflicts
// with the earlier conclusion `.spaceWidth = px`.
currentWidth += currentLine.length + spaceWidth;
width = Math.max(maxWidth, currentWidth);
}
lines.push(currentLine);
return { lines, width };
}
After fixing the bug, let's look at how the compiler can analyze the last function:
// No unit annotations on this!
function formatBulletPoint(nestingLevel: number, text: string, maxWidth: number, font = Font.default) {
// Based on the definitions of `indentSize` and `bulletWidth`, the
// compiler decides `nestingLevel` is dimensionless and `indent` is 'px'.
let indent = nestingLevel * indentSize + bulletWidth;
// `lines` has a polymorphic unit and `width` is 'px'
let { lines, width } = wordWrap(text, maxWidth - indent, font);
return {
indent, // 'px'
lines, // '.x'
height: lines.length * font.lineHeight, // 'px'
width: indent + width, // 'px'
};
}
Unit definitions should be optional, but are useful for specifying relationships like yard = 3 foot = 3 ft = 36 inch = 36 in
or W = Watt = J / s = kg m^2 s^−3
, and preferences like "aggressively replace 'kg m^2 s^−3' with 'J'". Also, assuming conversion factors can be defined, there could be syntax for mixing units with an auto-conversion factor, e.g. let totalGrams = grams + **pounds
could mean let totalGrams = grams + 453.592*pounds;
if the compiler had been told that unit lb = 0.453592 kg
and unit kg = 1000 g
. Maybe it's not perfectly logical, but I'd also propose the syntax **'g/lb' to mean 453.592'g/lb' which, of course, would lower to JavaScript simply as 453.592.
Units representing absolute axes (locations) can be useful, and behave differently from normal quantities. For example, I can add 21 bytes to 7 bytes and get 28 bytes, but what would it mean to add Earth latitude 59 to Earth latitude 60? You can subtract them to get a delta of 1 degree, representing about 111111 metres, but adding them is almost certainly a mistake.
I'm thinking unary !
can represent an absolute unit, i.e.
C
and K
can mean "celcius degrees (relative)" while !C
means "degrees celcius (absolute)" and !K
is the absolute temperature scale.lat
can mean "latitude degrees (difference)" while !lat
means "degrees latitude (absolute)", and you can define an alias unit Lat = !lat
.unit X = !x
defines X
as an absolute equivalent for x
, i.e. "the x axis".Proposed rules for absolute units !x
vs normal units x
:
x + x => x
, but !x + !x => !! x
and !x + x => !x
x - x => x
, !x - !x => x
and !!x - !x => !x
x * x => x^2
and x / x => 1
, but !x * !x
and !x / !x
are...maybe illegal? !x * x
is a different axis !(x^2)
!x
values are not directly assignable or comparable to x
values1'x' < 2'!x'
or value'!x' = 7'!!x'
) are illegalx
times a literal (with no unit specified) doesn't change the unit, x * 5
should have unit '!!!!!X'
if x
is absolute.Dimensionless units could be used to tag "minor" facts about something. For example, x
would be useful as a tag meaning "horizontal", so that x px
means "horizontal pixels". Likewise for units like y
or rad
(radian
). Such tags can be silently added or removed. You can also define units as multiples of dimensionless, e.g. unit rad = 180/3.141592653589793 degree = 1
defines rad
as a normal dimensionless unit and degree
as a multiple of it (edit: on second thought this doesn't feel quite right, but I'm not sure how the syntax can clearly communicate which unit is the "base" unit and which one is the multiple, and btw mathematicians always treat radians as the base unit). In that case, let r 'rad' = 360 'deg'
or let d 'deg' = 1 '1'
would be an error, but let r 'rad' = 1 '1'
is allowed.
These would be more useful with a way to indicate "mutual suspicion" or "different domains". Suppose unit x -|- y
means that x
and y
are dimensionless and in "different domains". Then you could write let x'x px' = 8'px'
or let x'px' = 8'x px'
, but let x'x px' = 8'y px'
would be an error. This only seems useful as a subtype of dimensionless units, since things like let x'kg' = 8'lb'
are already errors. Note: x -|- y
would not mean that the units can't be combined, e.g. 3'px x' * 5'px y' === 15'px x y'
, but the expression 5'px y' < 15'px x y'
would be an error.
Edit: this offers better type checking if you have multiple axes. Let's explore this with a unit-aware point type XY
:
unit x -|- y -|- z; // The usual axes
// Distinguish screen space, client space (e.g. coordinates in a text box),
// game world space and model space (e.g. coordinates in a character model).
unit screen -|- client -|- world -|- model;
export class XY {
// Assume 'this' refers to the polymorphic unit of the class instance (`this`).
constructor(public x: number'x this', public y: number'y this') { }
add(o: XY) { return new XY(this.x + o.x, this.y + o.y); }
sub(o: XY) { return new XY(this.x - o.x, this.y - o.y); }
mul(o: XY) { return new XY(this.x * o.x, this.y * o.y); }
mul(v: XY|number) {
if (typeof v === 'number')
return new XY(this.x * v, this.y * v);
else
return new XY(this.x * v.x, this.y * v.y);
}
// btw:
// Handling absolute units would be tricky for the compiler here, assuming
// the compiler supports polymorphic absoluteness. If we define
// - `?x` to get the "absoluteness number" of 'x' (e.g. ?!world = 1)
// - `N!x` to set absoluteness to N (2!world = !!world = 2!!!!!world)
// Then
// - `add` has a constraint `0!this = 0!o` and returns '(?this + ?.o)!this'
// - `sub` has a constraint `0!this = 0!o` and returns '(?this - ?.o)!this'
// - `mul` needs a separate analysis for `v: number` and `v: XY`. On the
// first return path it has a constraint `?.v = 0` if we make the
// simplifying assumption that there is a language-wide constraint
// `?.b = 0` for all `a * b`, i.e. absolute units must be on the left.
// Next hurdle: it returns '(?this * v)! this .v' which depends on the
// _value_ of v! This implies that if ?this = 0, the result is always
// relative, but if ?this != 0, v directly sets the absoluteness of the
// result, so the absoluteness is unknown unless v is a constant. The
// second return path ends up working basically the same way except that
// presumably v cannot be a constant, so the output x coordinate has
// unit '(?this * v.x)! x^2 this .v' and the y coordinate has unit
// '(?this * v.y)! y^2 this .v', which a compiler could reasonably
// combine into a final result '0! this .v' with constraint '?this = 0'.
};
let screenPoint = new XY(10, 15) '!screen'; // a location in screen space
let screenVec = new XY(5, -5) 'screen'; // a relative location in screen space
let screenPoint2 = screenPoint.add(screenVec); // has unit '!screen'
let screenVec2 = screenPoint.sub(screenPoint2); // has unit 'screen'
let worldPoint = new XY(709, 1785) '!world'; // a point in game-world space
let worldVec = new XY(9, 17) 'world'; // a vector in game-world space
let worldVec2 = worldVec.mul(3''); // has unit 'world'
let worldPoint3 = worldPoint.mul(3''); // has unit '!!!world'
let weird = worldVec.mul(screenVec); // has unit 'world screen'
let x = 1+2; // not a constant
let bad1 = worldPoint.mul(x); // unit error (argument must be constant)
let bad2 = worldPoint.mul(screenVec); // unit error (requires non-absolute this)
let bad3 = worldPoint.add(screenPoint); // unit error (incompatible units)
Edit: it occurs to me that people may want some types to be "independent" of units, with other types "having" units. For example, x as number
is probably not intended to change the unit of x, but one would want to be able to write m as Map<string, number'bytes'>
, implying that the type parameter V
has a unit so that value as V
inside the definition of Map
would change the unit to V
's unit. More thought is required about this. It also seems syntactically logical (but potentially confusing) to be able to define types that "are" units, like type Kg = unit 'kg'
so that BigInt & Kg = BigInt 'kg'
.
Edit: "Tag units" that can be silently removed, but not added, would be useful too, e.g. "validated strings". However, after re-reading #4895 while ignoring the phrase "tags can be assigned at runtime" and fixing syntax errors in asNonEmpty
, I see that TypeScript already supports this kind of compile-time tag pretty well via empty enums. Even so, I leave this example as food for thought:
unit email = tag<email>; // or whatever
// Ideally this would involve union types that have different units in
// different branches, as opposed to units being completely independent as I
// suggested earlier. Otherwise the return unit must be 'email' even if
// validation fails. Maybe that's fine, but the issue requires careful
// consideration because it may be impractical to change the design later.
// If T 'x' _sets_ the unit of T to 'x', a different syntax such as T '*x' is
// necessary to _combine_ the unit of T with 'x'. `typeof s` may appear to be
// equivalent to `string`, but I'm assuming instead that `typeof` gets both
// the type and the unit (remember, `s` has an implicit "unit parameter" `.s`)
function emailValidationFilter(s: string): undefined | typeof s '*email' {
return s.match(emailRegex) ? s as typeof s '*email' : undefined;
}
const emailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
type ContactInfo = { email?: string 'email', ... }
function Foo(contact: ContactInfo) {
let not_email: string'' = contact.email; // OK
contact = { email: 'foo' }; // error!
}
// Units can be used without being defined; `unit` just specifies settings.
unit pixel = px; // define synonyms
unit kg = kilo = 1000 g = 1000 gram; // define synonyms and ratios
unit $USD = $; // $ is an identifier character as usual
unit sq.km = km^2; // dot is allowed in the middle, ^ is exponent
unit X = !x; // define 'X' as an absolute 'x' axis
// defines relationship between !C and !F, and implies C = F * 5/9
unit !C = (!F - 32) * 5/9;
unit kg m^2 s^−3 => J; // request unit replacement whenever possible
let weight: number 'kg'; // unit as type suffix
let distance: number 'm | yd'; // union unit suffix
let size: unit 'KiB'; // use unit in a context where a type was expected
let kph = distance'km' / 24'hr'; // unit assertion suffix expressions
let speed 'm/s'; // unit without type (equivalent to `let speed: unit 'm/s'`)
let totalGrams = grams + **pounds; // auto unit conversion
let fahr '!F' = **celciusTemperature; // auto unit conversion
const gramsPerPound = **'g/lb'; // get unit conversion factor constant
let foo = x as number '*u'; // unitof foo = unitof ((x as number) * 1'u')
function sizeOf(x: any) 'bytes' {...} // set return unit but not return type
// Is special support for nonlinear functions necessary?
let three 'log $' = Math.log10(1000'$');
This feature request is similar to units of measure in F#.
For example:
(Moved from work item 1715 on Codeplex.)
Proposal
Last Updated: 2016-06-09 Copied from: https://github.com/dsherret/Units-of-Measure-Proposal-for-TypeScript
Overview
Units of measure is a useful F# feature that provides the optional ability to create tighter constraints on numbers.
TypeScript could benefit from a similar feature that would add zero runtime overhead, increase type constraints, and help decrease programmer error when doing mathematical calculations that involve units. The feature should prefer explicity.
Defining Units of Measure
Units of measure should probably use syntax similar to type aliases (#957). More discussion is needed on this, but for the purpose of this document it will use the following syntax:
The optional measure expression part can be used to define a new measures in terms of previously defined measures.
Example Definitions
Units of measure can be defined in any order. For example,
a
in the example above could have been defined beforem
ors
.Circular Definitions
Circular definitions are NOT allowed. For example:
Use with Number
Units of measure can be defined on a number type in any of the following ways:
TODO: Maybe we shouldn't use the
<m>
syntax because it might conflict with jsx files.Detailed Full Example
Use With Non-Unit of Measure Number Types
Sometimes previously written code or external libraries will return number types without a unit of measure. In these cases, it is useful to allow the programmer to specify the unit like so:
Dimensionless Unit
A dimensionless unit is a unit of measure defined as
number<1>
.Scope
Works the same way as
type
.External and Internal Modules
Also works the same way as
type
.In addition, if an external library has a definition for meters and another external library has a definition for meters then they should be able to be linked together by doing:
TODO: The above needs more thought though.
Definition File
Units of measure can be defined in TypeScript definition files (
.d.ts
) and can be used by any file that references it. Defining units of measure in a definition file is done just the same as defining one in a.ts
file.Compilation
The units of measure feature will not create any runtime overhead. For example:
Compiles to the following JavaScript:
Math Library
Units of measure should work well with the current existing Math object.
Some examples: