Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Units of measure #364

Open
dsherret opened this issue Aug 5, 2014 · 59 comments
Open

Suggestion: Units of measure #364

dsherret opened this issue Aug 5, 2014 · 59 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@dsherret
Copy link
Contributor

dsherret commented Aug 5, 2014

This feature request is similar to units of measure in F#.

For example:

const metres  = 125<m>;
const seconds = 2<s>;
let speed: number<m/s>;

speed = metres / seconds;          // valid
speed = metres / seconds / 10<s>;  // error -- cannot convert m/s**2 to m/s

(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:

type measure <name> [ = measure expression ];

The optional measure expression part can be used to define a new measures in terms of previously defined measures.

Example Definitions

type measure m;
type measure s;
type measure a = m / s**2;

Units of measure can be defined in any order. For example, a in the example above could have been defined before m or s.

Circular Definitions

Circular definitions are NOT allowed. For example:

type measure a = b;
type measure b = a; // error

Use with Number

Units of measure can be defined on a number type in any of the following ways:

type measure m;

// 1. Explicitly
let distance: number<m> = 12<m>;
// 2. Implictly
let distance = 12<m>;
// 3. Using Number class
let distance = new Number(10)<s>;

TODO: Maybe we shouldn't use the <m> syntax because it might conflict with jsx files.

Detailed Full Example

type measure m;
type measure s;
type measure a = m / s**2;

let acceleration = 12<a>,
    time         = 10<s>;

let distance = 1/2 * acceleration * (time ** 2); // valid -- implicitly typed to number<m>
let avgSpeed = distance / time;                  // valid -- implicitly typed to number<m/s>

time += 5<s>;         // valid
time += 5;            // error -- cannot convert number to number<s>
time += distance;     // error -- cannot convert number<m> to number<s>

// converting to another unit requires asserting to number then the measure
time += (distance as number)<s>; // valid

acceleration += 12<m / s**2>;         // valid
acceleration += 10<a>;                // valid
acceleration += 12<m / s**2> * 10<s>; // error -- cannot convert number<m/s> to number<a>

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:

type measure s;

let time = 3<s>;

time += MyOldLibrary.getSeconds();    // error -- type 'number' is not assignable to type 'number<s>'
time += MyOldLibrary.getSeconds()<s>; // valid

Dimensionless Unit

A dimensionless unit is a unit of measure defined as number<1>.

let ratio = 10<s> / 20<s>; // implicitly typed to number<1>
let time: number<s>;

time = 2<s> * ratio;         // valid
time = time / ratio;         // valid
time = (ratio as number)<s>; // valid
time = 2<s> + ratio;         // error, cannot assign number<1> to number<s>
time = ratio;                // error, cannot assign number<1> to number<s>
time = ratio<s>;             // error, cannot assert number<1> to number<s>

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:

import {m as mathLibraryMeterType} from "my-math-library";
import {m as mathOtherLibraryMeterType} from "my-other-math-library";

type measure m = mathLibraryMeterType | mathOtherLibraryMeterType;

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:

type measure cm;
type measure m;

let metersToCentimeters = 100<cm / m>;
let length: number<cm> = 20<m> * metersToCentimeters;

Compiles to the following JavaScript:

var metersToCentimeters = 100;
var length = 20 * metersToCentimeters;

Math Library

Units of measure should work well with the current existing Math object.

Some examples:

Math.min(0<s>, 4<m>); // error, cannot mix number<s> with number<m> -- todo: How would this constraint be defined?

let volume = Math.pow(2<m>, 3)<m**3>;
let length = Math.sqrt(4<m^2>)<m>;
@danquirk
Copy link
Member

danquirk commented Aug 5, 2014

Definitely love this feature in F#. Would need to pick some syntax for how you define these and make sure we're not too at risk for future JS incompatibility with whatever is picked.

@basarat
Copy link
Contributor

basarat commented Aug 6, 2014

👍

@electricessence
Copy link

Not sure if this should be in TypeScript. You can use classes to manage this:
https://github.com/electricessence/TypeScript.NET/blob/master/System/TimeSpan.ts
https://github.com/electricessence/Open.Measuring/blob/master/Measurement.ts

@dsherret
Copy link
Contributor Author

dsherret commented Aug 6, 2014

You could, but using a class is a lot of overhead for doing calculations while ensuring type. It's also much more code to maintain and it's barely readable when doing complex calculations.

A units of measure feature adds no runtime overhead and—in my opinion—it would make the language much more attractive.

@zpdDG4gta8XKpMCd
Copy link

Meanwhile in order to simulate units of measure, thank to the dynamic nature of JavaScript, you can use interfaces and the trick to get nominal types

interface N<a> { 'i am a number measured in': a }
function numberOf<a>(value: number): N<a> { return <any>value; }
function add<a>(one: N<a>, another: N<a>) : N<a> { return <any>one + <any>another; }
interface Ft { 'i am a foot ': Ft }
interface M { 'i am a meter ': M }
var feet = numberOf<Ft>(2);
var meters = numberOf<M>(3);
feet = meters; // <-- a problem

unfortunately due to 'best common type' resolution (which hopefully is going to be fixed) the following wont be prevented:

var huh = add(feet, meters);

however this will be

var huh = add<Ft>(feet, meters); // <-- problem

@dsherret
Copy link
Contributor Author

dsherret commented Aug 8, 2014

Some suggested syntax:

declare type m;
declare type s;
declare type a = m/s^2;

var acceleration = 12<a>,
    time         = 10<s>;

var distance = 1/2 * acceleration * time * time; // distance is implicitly typed as number<m>

This could also allow for tiny types like so:

declare type email;

function sendEmail(email: string<email>, message : string) {
    // send the email in here
}

var myEmail = "[email protected]"<email>;
sendEmail(myEmail, "Hello!");           // valid
sendEmail("some string", "Hello!");     // invalid

Some outstanding questions I can think of:

  1. Should this feature be allowed on types other than number? Maybe it should be allowed on just number and string?
  2. If it's allowed on additional types, how does this feature work with a type that has generics? (Side note: when would this even be useful?)

@zpdDG4gta8XKpMCd
Copy link

@dsherret, unit measures are meant for primitive types for better type safety, more complex custom structures (including generics) don't need it, however standard built-in complex types might benefit from it too, so:

  1. yes, for all primitive types including number, boolean and string (undefined, void, and null might have it too)
  2. yes for standard types, sometimes it seems useful to assign a unit to an instance of Date for example

@dsherret
Copy link
Contributor Author

dsherret commented Aug 8, 2014

@Aleksey-Bykov ah yeah, I forgot about how it could be useful for boolean too. Date makes sense to me as well because you can't extend dates. Other than that, I don't see much use for it with anything else (including undefined, void, and null).

So:

  • String
  • Number
  • Boolean
  • Date

@dsherret
Copy link
Contributor Author

dsherret commented Aug 9, 2014

I have started to write a proposal for this feature. Please offer your suggestions and criticisms. I've tried to make it similar to F#:

https://github.com/dsherret/Units-of-Measure-Proposal-for-TypeScript

I'm not at all familiar with the typescript compiler so I don't know how much of an impact this feature would have on it.

@saschanaz
Copy link
Contributor

@dsherret, I think declare type m syntax would be able to cover other features such as typedef (#308). How about declare type m extends number, to get compatibility with potential other features? :D

Or, even without declare.

/* 
Types defined by this syntax can extend only one of the primitive types, or, only `number` for this feature.
`m` and `s` should be treated as two different types, both derived from `number`.
`m` and `s` should also be discriminated against `number`.
*/
type m extends number;
type s extends number;

/* Mathematic operators in type definition creates new type. */
type a = m / s ^ 2;
// Note: Wouldn't caret here be confusing, as it still works as a XOR operator in other lines?
var acceleration = <a>12;
var time = <s>10;

time += <s>5; // Valid
time += 5; // Error, not compatible
/* ... */

dsherret added a commit to dsherret/Units-of-Measure-Proposal-for-TypeScript that referenced this issue Aug 9, 2014
@dsherret
Copy link
Contributor Author

Caret

I thought using caret might be confusing because it's usually used as the XOR operator, but to me the benefit of readability outweighs the potential confusion.

// This seems more readable to me:
type V = (kg * m^2) / (A * s^3);
// than this:
type V = (kg * m * m) / (A * s * s * s)

However, yeah it might cause some confusion when it's actually used in a statement:

var area = 10<m^2> + 20<m^2>;

Though I think the caret being within the angle brackets makes it fairly obvious that it's not the XOR operator, though I know some people would definitely think that on first glance. It is nicer than writing this:

var area = 10<m*m> + 20<m*m>;

and I think only allowing that would cause some people to write definitions like so (which I think would look gross in the code):

type m2 = m * m;
type m3 = m2 * m;

Definition

I thought using the declare keyword at the front was just a good way to piggy back on ambient definitions in order to avoid conflicts with any potential JS changes in the future; however, I think the chances of there being a type keyword is really low. Your shortening makes sense to me.

Here's some other alternatives I can think of:

type number<m>;
type string<email>;
// -- or
type m     : number;
type email : string;

Before or after

I think doing this:

var distance = 100<m> + 20<m/s> * 10<s>;

...is more readable when visualizing the mathematics and aligns more with F#.

Doing this makes more sense with the statement type s extends number, but it's not as readable:

var distance = <m>100 + <m/s>20 * <s>10;

I don't know... I guess we can keep coming up with ideas for all of this.

@dsherret
Copy link
Contributor Author

By the way, do you think it might be confusing to even referencing units of measure and tiny types as "types" (even though it's done in F#). Usually in javascript, when I think of a type, it's something I can use in plain javascript like: var t = new TypeName();. I think it might be good to separate this from the idea of typedefs.

@saschanaz
Copy link
Contributor

I agree that m^(n) is much more readable than m*m*m*.... Caret is anyway being used as power operator in other areas. Caret is also more readable than Math.pow.

When it comes to typing, I think that's not always true. We cannot do new number, or new string. However, we can just allow var t = new TypeName();, if it is important. For example:

type s extends number;
var time = new s(3); // still `s`, discriminated against normal number type.
console.log(time.toFixed(0));

would be converted to:

var time = new Number(3);
console.log(time.toFixed(0));

However, I think we have to discuss more to decide whether this is really needed.

@ivogabe
Copy link
Contributor

ivogabe commented Aug 10, 2014

I have a few questions for the ideas in this topic:

  • When is a measure in the scope of a document? If a type is defined in an inline module, is it visible from outside? Or if it's defined in an external module (commonjs or amd)?
  • Why do you use the type keyword? Maybe a measure keyword would be better?
  • How do you represent a measure that is dimensionless? For instance, the result of s / s. F# uses <1>.
  • Is it really useful to allow units on other types than numbers? When you call a variable myEmail you already know it's an email address.
  • When you have a unit of measure named m, is it allowed to have an interface, class, module or variable with the same name?
  • Is the following allowed:
measure a = b / c;
measure b = a * c;
measure c = b / a;

Or is this circular pattern not allowed?

My answers would be:

  • A measure is in the scope of a module. Outside the module it is not visible. When using external modules, you should create measures in a .d.ts file.
  • Use F#'s <1>. Not that this is not the same as an untyped variable.
  • Only allow measures on numbers. This makes the type system a lot easier. Also this prevents mistakes when you combines generics and measures.
  • It's not allowed to have a unit and an interface, class etc. that have the same name.
  • Circular patterns are not allowed, this code is valid:
measure a;
measure b = a * c; // usage before definition is allowed, as long as it's not a circular dependency.
measure c;

@saschanaz How can you create a variable with the measure m/s? new m/s(3) looks a bit strange to me.

I'd prefer the unit of measure to be after the number literal or expression, like @dsherret suggested.

@saschanaz
Copy link
Contributor

@ivogabe, Right, I think just <m/s>3; is good, or maybe <m/s>(new Number(x)); if we really need constructors. I personally think people would not want to do new s(3); as not all TypeScript types have constructors.

@dsherret
Copy link
Contributor Author

@ivogabe great questions!

Scope

I've been wondering about this myself. For example, what should happen in the following case:

module MyModule {
    export class MyClass {
        myMethod() : number<m/s> {
            return 20<m/s>;
        }
    }
}

Just throwing out some ideas here, but maybe measures could be imported in order to prevent conflicts between libraries... so you would have to write something like import m = MyMeasureModule.m; at the top of each file you want to use it. Then when you're writing a measure in a module you would do this:

module MyModule {
    export measure m;
    export measure s;

    export class MyClass {
        myMethod() : number<m/s> {
            return 20<m/s>;
        }
    }
}

That could help prevent conflicts because you could do import m = MyMeasureModule.m and import meters = SomeOtherLibrary.m, but it wouldn't be so nice when a conflict occurs. Additionally, it wouldn't be that nice to have to rewrite measure statements at the top of each file you want to use them in, but I guess it's not too bad (think of it like the necessity of writing using statements in c#).

type keyword vs other keywords

I do like the measure keyword more specifically for units of measure, but I was trying to think of how to use it in combination with something like the annotations as outlined in the original codeplex case... so I just temporarily rolled with the type keyword like what F# does for units of measure. Any ideas for what might be good? Or maybe annotations should be separate from a units of measure feature?

Dimensionless measure

Another suggestion would just to make it a plain old number. For example:

var ratio = 10<s> / 20<s>; // ratio is now just type number and not number<s/s>

Other than number

I think it can be useful. A lot of other developers would disagree. It basically adds another level of constraint... so the compiler basically forces the developer to say "yes, I really want to do this". For example, when sending an email:

SendEmail("Some message", "[email protected]");                   // compile error
SendEmail("Some message"<message>, "[email protected]"<emailTo>); // whoops i mixed up the arguments... still getting a compile error
SendEmail("[email protected]"<emailTo>, "Some message"<message>); // that's better... no compile error

It's something I would definitely use, but if nobody else in the world would I'm fine with not focusing on this and just looking at a units of measure feature. I do think it would be a great tool to use to help make sure the code that's being written is correct.

Same name as class, module, or variable

I agree with what you said. It could get confusing to allow the same name.

Circular patterns

I agree again.

Conclusion

I'm going to start collecting all the ideas outlined in this thread and then someone on the typescript team can widdle it down to what they think would be good.

@ivogabe
Copy link
Contributor

ivogabe commented Aug 10, 2014

Another suggestion would just to make it a plain old number.

I think there should be a difference between a number without a measure and a number with a dimensionless measure. For example, I think a number without a measure can be casted to any measure, but a dimensionless measure can't.

var time: number<s> = 3<s>;
var ratio: number<1> = 10<s> / 20<s>; // dimensionless
var someNumber: number = 3; // no measure

var distance: number<m>;
distance = time<m>; // Error
distance = ratio<m>; // Error
distance = someNumber<m>; // Ok, since someNumber didn't have a measure

@dsherret
Copy link
Contributor Author

@ivogabe Ok, I see the point of that now. That makes a lot of sense. So it won't work when assigned directly to a number with a dimension, but it will work when used with another measure (or combination of) that has a dimension. For example:

var ratio = 10<s> / 20<s>,
    time : number<s>;

time = 2<s> * ratio; // works
time *= ratio;       // works
time = 2<s> + ratio; // error, cannot add number<1> to number<s>
time = ratio;        // error

By the way, do you think it's a good idea to allow people to change a variable's measure after it's been created? In your example, don't you think the programmer should define someNumber as number<m> from the start? I just think it could lead to confusing code.

Also, another point I just thought of... we'll have to look into how a units of measure feature would work with the math functions. For example, this shouldn't be allowed:

Math.min(0<s>, 4<m>);

...and power should work like this:

var area = Math.pow(2<m>, 2); // area implicitly typed to number<m^2>

@ivogabe
Copy link
Contributor

ivogabe commented Aug 10, 2014

By the way, do you think it's a good idea to allow people to change a variable's measure after it's been created?

I would propose a rule that it's allowed to add a measure to an expression that doesn't have a measure. So you don't change the measure (since it didn't have a measure) but you add one. 3<s> already does this: 3 is an expression that doesn't have a measure. This is also for usage with older code, or cases when the measure can't be determined. Example:

var time = 3<s>; // '3' is an expression that doesn't have a measure, so a measure can be added.
// Old function that doesn't have info about measures
function oldFunction(a: number, b: number) {
    return a + b;
}
time = oldFunction(4<s>, 8<s>)<s>;
// Function that returns a value whose measure cannot be determined.
var someNumber = 8;
var result = Math.pow(time, someNumber)<s^8>;

dsherret added a commit to dsherret/Units-of-Measure-Proposal-for-TypeScript that referenced this issue Aug 10, 2014
Made changes based on this thread: microsoft/TypeScript#364

- Added more examples
- Removed tiny types (maybe this can be added in later?)
@dsherret
Copy link
Contributor Author

@ivogabe Yes, that's true. It would also be useful when using external libraries that return values without units of measure defined.

By the way, what are your thoughts of defining them as such:

unit <unit-name> [ = unit ];

For example, unit m; instead of measure m;? I think the word unit more accurately describes what it is... but it looks an awful lot like uint...

@saschanaz
Copy link
Contributor

I think generics can help Math functions if we keep units as types.

// My old proposal being slightly changed: extends -> sorts.
type second sorts number;

/*
  T here should be number type units.
  `T extends number` here would include normal numbers, while `sorts` would not.
*/
interface Math {
  min<T sorts number>(...values: T[]): T;
  min(...values: number[]): number;
}

I'm not sure about Math.pow, however. Maybe we just have to give normal number type and let it be casted, as @ivogabe did.

// This is just impossible.
pow<T ^ y>(x: T, y: number): T ^ y;

// `T extends number` now explicitly includes normal numbers.
pow<T extends number>(x: T, y: number): number;

@ivogabe
Copy link
Contributor

ivogabe commented Aug 11, 2014

@dsherret That doesn't matter to me, I just chose measure because F# uses it and because unit could be confused with unit tests.

@saschanaz In my opinion units shouldn't be types, but you can use them as a type, like number<cm>. That makes it easier to combine this proposal with the proposal for integers (and doubles), you can write integer<cm> for instance. Also number<cm> extends number, so this can be used with generics, like in your second pow example.

I don't see a scenario where a sorts keyword would be necessary. Like in your Math.min example, most functions should allow numbers with a unit and numbers without, we shouldn't force a user to use units of measure.

@saschanaz
Copy link
Contributor

Normally, T extends I works as:

interface Foo { /* */ }
interface Bar extends Foo { /* */ }
function someFunction<T extends Foo>(x: T, y: T): T { /* */ }

var foo: Foo;
var bar: Bar;
someFunction(foo, bar); // returns Foo

I think we don't want to allow Math.min(3<cm>, 3); to return normal number. We can just block this, but wouldn't that be confusing as extends doesn't work so in other cases?

By the way, I like integer<cm>. That convinces me. Let me think more about it.

@ivogabe
Copy link
Contributor

ivogabe commented Aug 11, 2014

I think we don't want to allow Math.min(3<cm>, 3); to return normal number

Well, it may be better to allow this, for backwards compatibility. When you change, for instance, Date's getSeconds method to getSeconds(): number<s>; the following code would fail:

var date = new Date();
var seconds = Math.min(date.getSeconds(), 30);

I think backwards compatibility is important for this feature since we don't want to break old code and we don't want to force people using units of measure.

One question would be, should the following code compile:

var date = new Date();
var value = Math.min(date.getSeconds(), date.getHours());

Backwards compatibility versus a logical implementation.

@dsherret
Copy link
Contributor Author

To not force a developer to use units of measure and to allow for backwards compatibility, I don't think date.getSeconds() should return number<s>. If a developer really wants this, they should create a "units of measure date definition file" doing something like:

declare class Date { getSeconds: () => number<s>; /* etc... */ }

Not doing so would definitely break a lot of existing code.

I'm thinking there might be scenarios where a developer actually wants to do something like Math.min(3<s>, 2<h>);. In this case, I think the developer should have to cast these values to a number without a unit of measure.

@saschanaz
Copy link
Contributor

Note: another proposal also uses extends to define mini types. (#202)

It's already mentioned here, but just to note ;)

@plalx
Copy link

plalx commented Nov 14, 2015

Is there any news about this?

@zpdDG4gta8XKpMCd
Copy link

👍
forgive my audacity, this (proposal) looks like a good topic to consider // cc @RyanCavanaugh

@ozyman42
Copy link

ozyman42 commented Jul 3, 2016

This could be very very useful if combined with React-Radium for CSS Units of measure being used inline with JS.

@gasi
Copy link

gasi commented Oct 7, 2016

I’d also like to see this included in the language as I found this very powerful in Haskell, e.g.

Statically differentiate between say URLs newtype URL = URL String and database IDs newtype PersonId = PersonId String (Haskell) without runtime overhead: loadPerson(databaseURL: URL, id: PersonId) vs loadPerson(databaseURL: string, id: string) (TypeScript) which can cause user errors when applying arguments of the same type with different meanings in the wrong order.

@zpdDG4gta8XKpMCd
Copy link

@gasi this is already possible via type tagging: #4895

@gasi
Copy link

gasi commented Oct 7, 2016

@Aleksey-Bykov Thanks, I’ll check it out 😄

@zpdDG4gta8XKpMCd
Copy link

keep in mind, since type tagging is an official hack there are a few flavors of how it can be done: #8510, #202 (comment), #202 (comment)

funny fact is that hacks like this are officially discouraged (hi @RyanCavanaugh , i am still using T|void):

@vultix
Copy link

vultix commented Jan 3, 2018

Has there been any updates on this proposal? I've been using one of the hacks that @Aleksey-Bykov shared, but would love to have this ability built into typescript.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 5, 2018

this what our latest workaround looks like:

declare global {

    declare class In<T> { private '____ in': T; }
    declare class Per<T> { private '____ per': T; }
    declare class As<T> { private '____ as': T; }
    type Delta = As<'quantity'>;

    type MegabitsPerSecond = number & In<'megabit'> & Per<'second'>;
    type MegasymbolsPerSecond = number & In<'megasymbol'> & Per<'second'>;
    type Megahertz = number & In<'megahertz'>;
    type Pixels = number & In<'pixel'>;
    type Decibels = number & In<'decibel'>;
    type ChipsPerSymbol = number & In<'chip'> & Per<'symbol'>;
    type PixelsPerMegahertz = number & In<'pixel'> & Per<'megahertz'>;
    type Milliseconds = number & In<'millisecond'>;
    type PixelsPerMillisecond = number & In<'pixel'> & Per<'millisecond'>;

    interface Number {
        plus<U>(this: number & In<U> & Delta, right: number & In<U> & Delta): number & In<U>;
        plus<U>(this: number & In<U> & Delta, right: number): number & In<U>;
        plus<U>(this: number & In<U>, right: number & In<U> & Delta): number & In<U>;
        plus<U>(this: number & In<U>, right: number & In<U>): void; // <-- either param needs to be of `& Delta`
        plus(this: number, right: number): number;

        minus<U>(this: number & In<U>, right: number & In<U> & Delta): number & In<U>;
        minus<U>(this: number & In<U>, right: number & In<U>): number & In<U> & Delta;
        minus(this: number, value: number): number;

        dividedBy<U, V>(this: number & In<U> & Per<V>, value: number & In<U> & Per<V>): number;
        dividedBy<U, V>(this: number & In<U> & Delta, value: number & In<U> & Per<V>): number & In<V> & Delta;
        dividedBy<U, V>(this: number & In<U>, value: number & In<U> & Per<V>): number & In<V>;
        dividedBy<U, V>(this: number & In<U>, value: number & In<V>): number & In<U> & Per<V>;
        dividedBy<U, V>(this: number & In<U> & Per<V>, value: number): number & In<U> & Per<V>;
        dividedBy<U>(this: number & In<U>, value: number & In<U>): number;
        dividedBy(this: number, value: number): number;

        times<U, V>(this: number & In<U> & Per<V>, value: number): number & In<U> & Per<V>;
        times<U, V>(this: number & In<U>, value: number & In<V> & Per<U>): number & In<V>;
        times<U>(this: number & In<U>, value: number): number & In<U>;
        times<U>(this: number, value: number & In<U> & Delta): number & In<U> & Delta;
        times<U>(this: number, value: number & In<U>): number & In<U>;
        times(this: number, value: number): number;
    }
}

Number.prototype.minus = function minus(this: number, value: number): number {
    return this - value;
} as typeof Number.prototype.minus;

Number.prototype.plus = function plus(this: number, value: number): number {
    return this + value;
} as typeof Number.prototype.plus;

Number.prototype.times = function times(this: number, value: number): number {
    return this * value;
} as typeof Number.prototype.times;

Number.prototype.dividedBy = function dividedBy (this: number, value: number): number {
    return this / value;
} as typeof Number.prototype.dividedBy;

@fenduru
Copy link

fenduru commented Jan 24, 2018

You all might be interested in the unique symbol feature that's in 2.7

declare const as: unique symbol; // The type of this Symbol is essentially nominal
type As<T> = number & { [as]: T }

My use case for this is not for units of measure, but rather for dependency injection.

declare const associated: unique symbol;
type Injectable<T> = string & { [associated]: T }

const foo: Injectable<number> = 'foo';
const bar: Injectable<(number) => boolean> = 'bar';

...

inject([foo, bar], function(injectedFoo, injectedBar) {
  // TypeScript knows that injectedFoo is a `number`, and injectedBar is a `(number) => boolean`
}

The code for inject isn't super pretty, but will hopefully be made better by #5453 so I don't have to define N overloads to support variable number of dependencies.

interface inject {
  (dependencies: undefined[], () => void): void
  <A>(dependencies: [Injectable<A>], (a: A) => void): void
  <A, B>(dependencies: [Injectable<A>, Injectable<B>], (a: A, b: B) => void): void
}

@zpdDG4gta8XKpMCd
Copy link

@qm3ster i wish it had anything to do with unit of measures, from what it looks it just defines some algebra over some opaque types

@mindbrave
Copy link

mindbrave commented Sep 6, 2018

@Aleksey-Bykov I made this (and it works), but it requires your operations to base on my mul and div operations. Also it supports exponents in range <-4, 4> only, but you can extend it. Examples are at the bottom. It has NO runtime overhead. What do you think?

type Exponent = -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4;

type NegativeExponent<T extends Exponent> = (
    T extends -4 ? 4 :
    T extends -3 ? 3 :
    T extends -2 ? 2 :
    T extends -1 ? 1 :
    T extends 0 ? 0 :
    T extends 1 ? -1 :
    T extends 2 ? -2 :
    T extends 3 ? -3 :
    T extends 4 ? -4 :
    never
);
type SumExponents<A extends Exponent, B extends Exponent> = (
    A extends -4 ? (
        B extends 0 ? -4 :
        B extends 1 ? -3 :
        B extends 2 ? -2 :
        B extends 3 ? -1 :
        B extends 4 ? 0 :
        never
    ) :
    A extends -3 ? (
        B extends -1 ? -4 :
        B extends 0 ? -3 :
        B extends 1 ? -2 :
        B extends 2 ? -1 :
        B extends 3 ? 0 :
        B extends 4 ? 1 :
        never
    ) :
    A extends -2 ? (
        B extends -2 ? -4 :
        B extends -1 ? -3 :
        B extends 0 ? -2 :
        B extends 1 ? -1 :
        B extends 2 ? 0 :
        B extends 3 ? 1 :
        B extends 4 ? 2 :
        never
    ) :
    A extends -1 ? (
        B extends -3 ? -4 :
        B extends -2 ? -3 :
        B extends -1 ? -2 :
        B extends 0 ? -1 :
        B extends 1 ? 0 :
        B extends 2 ? 1 :
        B extends 3 ? 2 :
        B extends 4 ? 3 :
        never
    ) :
    A extends 0 ? (
        B extends -4 ? -4 :
        B extends -3 ? -3 :
        B extends -2 ? -2 :
        B extends -1 ? -1 :
        B extends 0 ? 0 :
        B extends 1 ? 1 :
        B extends 2 ? 2 :
        B extends 3 ? 3 :
        B extends 4 ? 4 :
        never
    ) :
    A extends 1 ? (
        B extends -4 ? -3 :
        B extends -3 ? -2 :
        B extends -2 ? -1 :
        B extends -1 ? 0 :
        B extends 0 ? 1 :
        B extends 1 ? 2 :
        B extends 2 ? 3 :
        B extends 3 ? 4 :
        never
    ) :
    A extends 2 ? (
        B extends -4 ? -2 :
        B extends -3 ? -1 :
        B extends -2 ? 0 :
        B extends -1 ? 1 :
        B extends 0 ? 2 :
        B extends 1 ? 3 :
        B extends 2 ? 4 :
        never
    ) :
    A extends 3 ? (
        B extends -4 ? -1 :
        B extends -3 ? 0 :
        B extends -2 ? 1 :
        B extends -1 ? 2 :
        B extends 0 ? 3 :
        B extends 1 ? 4 :
        never
    ) :
    A extends 4 ? (
        B extends -4 ? 0 :
        B extends -3 ? 1 :
        B extends -2 ? 2 :
        B extends -1 ? 3 :
        B extends 0 ? 4 :
        never
    ) :
    never
);

type Unit = number & {
    s: Exponent,
    m: Exponent,
    kg: Exponent,
};

// basic unit types
type Seconds = number & {
    s: 1,
    m: 0,
    kg: 0,
};
type Meters = number & {
    s: 0,
    m: 1,
    kg: 0,
};
type Kg = number & {
    s: 0,
    m: 0,
    kg: 1,
};

// unit operations
const add = <T extends Unit>(a: T, b: T) => (a + b) as T;
const sub = <T extends Unit>(a: T, b: T) => (a - b) as T;

type MultiplyUnits<A extends Unit, B extends Unit> = number & {
    s: SumExponents<A["s"], B["s"]>,
    m: SumExponents<A["m"], B["m"]>,
    kg: SumExponents<A["kg"], B["kg"]>,
};

type DivideUnits<A extends Unit, B extends Unit> = number & {
    s: SumExponents<A["s"], NegativeExponent<B["s"]>>,
    m: SumExponents<A["m"], NegativeExponent<B["m"]>>,
    kg: SumExponents<A["kg"], NegativeExponent<B["kg"]>>,
};

const mul = <A extends Unit, B extends Unit>(a: A, b: B): MultiplyUnits<A, B> => (a * b) as MultiplyUnits<A, B>;
const div = <A extends Unit, B extends Unit>(a: A, b: B): DivideUnits<A, B> => (a / b) as DivideUnits<A, B>;
const pow = <A extends Unit>(a: A): MultiplyUnits<A, A> => mul(a, a);

// # examples of usage #

// custom unit types
type MetersPerSecond = number & {
    s: -1,
    m: 1,
    kg: 0,
};
type SquaredMeters = number & {
    s: 0,
    m: 2,
    kg: 0,
};
type Newtons = number & {
    s: -2,
    m: 1,
    kg: 1,
};

const speedToDistance = (speed: MetersPerSecond, time: Seconds): Meters => mul(speed, time);
const calculateSpeed = (distance: Meters, time: Seconds): MetersPerSecond => div(distance, time);
const rectangleArea = (width: Meters, height: Meters): SquaredMeters => mul(width, height);

type Vec2<T extends number> = [T, T];

const addVec2 = <T extends Unit>(v1: Vec2<T>, v2: Vec2<T>): Vec2<T> => [add(v1[0], v2[0]), add(v1[1], v2[1])];
const scaleVec2 = <U extends  Unit, T extends Unit>(scale: U, v: Vec2<T>): Vec2<MultiplyUnits<T, U>> => [mul(v[0], scale), mul(v[1], scale)];
const divVec2 = <U extends  Unit, T extends Unit>(factor: U, v: Vec2<T>): Vec2<DivideUnits<T, U>> => [div(v[0], factor), div(v[1], factor)];

type PhysicalBody = {
    velocity: Vec2<MetersPerSecond>,
    mass: Kg
};

// error below because you cant add speed vector to acceleration vector
const applyForceError = (force: Vec2<Newtons>, duration: Seconds, body: PhysicalBody): PhysicalBody => ({
    ...body,
    velocity: addVec2(body.velocity, divVec2(body.mass, force))
});

// this one works because Newtons multiplied by Kilograms and Seconds equals Meters per Seconds, which is body velocity
const applyForce = (force: Vec2<Newtons>, duration: Seconds, body: PhysicalBody): PhysicalBody => ({
    ...body,
    velocity: addVec2(body.velocity, scaleVec2(duration, divVec2(body.mass, force)))
});

@zpdDG4gta8XKpMCd
Copy link

quite interesting, we haven't gotten that far, our arithetic is very simple

@mindbrave
Copy link

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

@atennapel
Copy link

atennapel commented Nov 12, 2018

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.

@DaAitch
Copy link

DaAitch commented Oct 29, 2020

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.

@cameron-martin
Copy link

Here is my user-space implementation of this: Playground.

@buge
Copy link

buge commented Aug 25, 2021

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.

@btakita
Copy link

btakita commented Jun 18, 2022

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

@RebeccaStevens
Copy link

Hey everyone, I thought I'd let you all know I've just release a new library to address uom types.
It's heavily inspired by @mindbrave's uom-ts; I called it uom-types.

Check it out here: uom-types
Feedback is most welcome.

@qwertie
Copy link

qwertie commented Jan 11, 2024

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:

Unit types should be independent from normal types

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 BigInts. 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.

It's probably better to support unit inference than unit checking

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 powers
  • y * 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
    • This implies .y = 1, and indirectly implies .z = .x * .x
  • so 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 & kinds of units

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 values
  • comparisons or assignments of different absolutes (1'x' < 2'!x' or value'!x' = 7'!!x') are illegal
  • whereas x 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!
}

Proposed syntax

// 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'$');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests