microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.26k stars 12.31k forks source link

Suggestion: Units of measure #364

Open dsherret opened 9 years ago

dsherret commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

:+1:

electricessence commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 = "david@email.com"<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 commented 9 years ago

@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 commented 9 years ago

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

dsherret commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

measure a = b / c;
measure b = a * c;
measure c = b / a;

Or is this circular pattern not allowed?

My answers would be:

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 commented 9 years ago

@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 commented 9 years ago

@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@email.com");                   // compile error
SendEmail("Some message"<message>, "email@email.com"<emailTo>); // whoops i mixed up the arguments... still getting a compile error
SendEmail("email@email.com"<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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

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

electricessence commented 9 years ago

The following classes (examples) could be extended to do even more, but the base unit of time in JavaScript is simply milliseconds just like the base unit of time in C# is ticks. https://github.com/electricessence/TypeScript.NET/blob/master/System/TimeSpan.ts Includes "ClockTime", "TimeUnit", "TimeUnitValue" and "TimeSpan".

electricessence commented 9 years ago

See another example of coercing time units here: https://github.com/electricessence/TypeScript.NET/blob/master/System/Diagnostics/Stopwatch.ts Just like in the C# version, you can call .elapsed (TimeSpan), but I've standardized it to a common interface that if you want total seconds, you simply call .elapsed.seconds and if you want only the number of seconds in the minute, you simply call .elapsed.time.seconds. But the Stopwatch still exposes .elapsedMilliseconds and other millisecond versions because milliseconds is the base unit in JS.

electricessence commented 9 years ago

Other measurement (unit) scenarios are covered in similar structures by using a "UnitType" enum. This allows for easy conversion from one unit to another as well as normalization in order to do math. https://github.com/electricessence/Open.Measuring/blob/master/Measurement.ts This has a static .convert function but also allows for passing of a "Measurement" around and converting back and forth. This has proved useful in doing certain math computations (like CFD) where you might be measuring a room using feet, or inches, but your math calculations have to be in millimeters.

electricessence commented 9 years ago

IMO, I don't think F# style units should be included in TypeScript, but that doesn't mean there couldn't be a forked version of the compiler that does it and then maybe it get's included. ;) The issues are that it feels kind of like a localization issue, not that there are any other units, but where do you stop at? Do you include all of physics? Joules, ftlbs, CFM, etc?

dsherret commented 9 years ago

The issues are that it feels kind of like a localization issue, not that there are any other units, but where do you stop at? Do you include all of physics? Joules, ftlbs, CFM, etc?

@electricessence in this feature all units and conversions are defined by the programmer. For example:

unit cm;
unit m;

var metersToCentimeters = 100<cm/m>;

var length = 20<m> * metersToCentimeters; // length is now 2000<cm>

There are no localization issues. The idea is for compile time errors when mixing improper units and doing so with zero runtime overhead. I think TypeScript could benefit greatly with such a feature.

electricessence commented 9 years ago

@dsherret: Interesting. I do see value in this. It would be really cool. ... After looking around, if you use C# as an example, there is no language level unit implementation. Hence the need for F#. More similar discussion: http://stackoverflow.com/questions/348853/units-of-measure-in-c-sharp-almost

If not built into the language, A possible solution could be some sort of markup and a compiler extension?

dsherret commented 9 years ago

@electricessence it's incredibly valuable. When doing lots of calculations it really helps to catch a programmers mistakes right away so I think this feature would definitely attract the math crowd. I think this feature in combination with classes similar to what you've created (and the discussion you linked to) would be very useful.

Hopefully this feature will make it into the language (it's slightly complex, but for what I see it doesn't conflict with anything currently existing). If not then yeah, we can always add to what has been created. I'd be up for trying to help with that effort if the time comes.

eggers commented 9 years ago

My feelings is that I would rather have the casting to a measure be implicit for type number. e.g., instead of:

var time:number<s> = 50<s>;

I would rather see this:

var time:number<s> = 50;
var speed:number<m/s> = 50 / time //compile error
dsherret commented 9 years ago

@eggers Implicit casting to a measure when the RHS is a literal of type number shouldn't cause any problems—unless maybe someone can think of one?

Also, agreed that var speed:number<m/s> = 50 / time; should be a compile error since the RHS would be of type number<1/s>.

Edit: the downside to doing that—var time:number<s> = 50;—is inconsistency with the syntax.

Edit2: Yeah, I don't think we should support this. I think people should write var time = 50<s>; instead if they want to be implicit.

RossRogers commented 9 years ago

Would this concept extend to other primitive types like the string type? Nominal typing in strings, as Joel Spolsky wrote about, is useful. Unsafe/safe, sanitized/un-sanitized, different kinds of strings for URLs or notes, or HTML, etc.

dsherret commented 9 years ago

@RossRogers we talked about that a bit before. I think it could be useful as well.

jbondc commented 9 years ago

Haven't looked at all examples but I could have a go at this.

I'd propose the following syntax:

type minutes = number where { assign(val) => val * 60; };

let a = <minutes>3; // 180 (a has type number)
a = 1; // 1 (type number)

let a: minutes = 3; // 180 (a has type minutes)
a = 1; // 60 (type minutes)

type cm = number where { assign(val) => val * 100; };
type m = number where { assign(val) => val * 1000; };

let meters: m = 100; // 100000
var metersToCentimeters = <cm>100 / m; // 0.1 (type number)
centimeters: cm = 1000 / m; // 0.1 (type cm)

var lengthNumber = <m>20 * metersToCentimeters; // length is now 2000 (type number)
lengthNumber = 21; // 21
var lengthCm: cm = <m>20 * metersToCentimeters; // length is now 2000  (type cm)
lengthCm = 21; // 2100

type boundedMinutes = 0...60 where { assign(val) => val * 60 };

let minBound: boundedMinutes  = 1; // 60
minBound = 70; // error: The constant '70' is not in '0...60'
minBound = 2; // 120
minBound = -1; // error: The constant '-1' is not in '0...60'
plalx commented 8 years ago

Is there any news about this?

zpdDG4gta8XKpMCd commented 8 years ago

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

ozyman42 commented 8 years ago

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

gasi commented 7 years ago

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 commented 7 years ago

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

gasi commented 7 years ago

@aleksey-bykov Thanks, I’ll check it out 😄

zpdDG4gta8XKpMCd commented 7 years ago

keep in mind, since type tagging is an official hack there are a few flavors of how it can be done: #8510, https://github.com/Microsoft/TypeScript/issues/202#issuecomment-173143983, https://github.com/Microsoft/TypeScript/issues/202#issuecomment-184352626

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

vultix commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 5 years ago

@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 commented 5 years ago

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