Closed jonaskello closed 4 years ago
I think extended units would be hard to use as it is now, becuase the Quantity type is expected to the type defined in this package. For example Amount.create
is defined as:
export function create<T extends Quantity>(
value: number,
unit: Unit.Unit<T>,
decimalCount: number | undefined = undefined
): Amount<T> {
So if there is an external unit with an external quantity it would not pass the constraint for T extends Quantity
because here Quantity
refers to the type defined in this package and it does not include externally defined Quantity types.
Any extended units should also be possible to use with the Format
and Serialize
modules, not sure how that would work. One way could be that the functions in those packages accepts a map of Units so you can call them with a map of Units defined in an external package. Today they internally use import * as Units from "./units"
as a map of units when doing lookups.
One way to make it easier to extend might be to not use T extends Quantity
but just T
instead. Not sure of the impact that would have?
@AdamLuotonen @geon Any thoughts on the above?
We can limit the number of available units by having the application creating and using it's own Units
module by re-exporting from the standard Units
module:
my-units.ts
export { Meter, CentiMeter } from "standard-units/length";
export { Celsius, Kelvin } from "standard-units/temperature";
my-unit.formats.ts
export { Meter, CentiMeter } from "standard-unit-formats/length";
export { Celsius, Kelvin } from "standard-unit-formats/temperature";
application.ts
import * as Units from "./my-units";
const amount1 = Amount.create(10.0, Units.Meter) // OK
const amount2 = Amount.create(10.0, Units.MilliMeter) // NOT OK
const label = Format.getUnitFormat(amount1.unit);
// Need to pass in your own Units
const lengthUnits = Format.getUnitsForQuantity("Length", Units);
It is also possible add your own new units to the file like this:
foo-quantity.ts
import * as Unit from "../unit";
export type Foo = "Foo";
export const Bar = Unit.createBase("Bar", "Foo", "bar");
export const Zoo = Unit.createBase("Zoo", "Foo", "zoo");
my-units.ts
import { Units } from "uom";
export { Meter, CentiMeter } from "uom/lib/units/length";
export { Celsius, Kelvin } from "uom/lib/units/temperature";
export * from "./foo-quantity";
This will fall apart when trying to use these units with Amount.createAmount<Quantity>()
because the quantity Foo
is not part of the Quantity
union-type.
You can create your own Quantity
union-type like this:
my-quantity.ts
import * as Units from "./my-units";
export { Temperature } from "uom/lib/units/temperature";
export { Length } from "uom/lib/units/length";
export { Foo } from "./foo-quantity";
export type Quantity =
| Units.Length
| Units.Temperature
| Units.Foo
But how do you make Amount
use that Quantity
type instead of the internal one?
We can try the re-export strategy for Amount
but we will probably have to wrap every function:
my-amount.ts
import { Amount } from "uom";
import { Quantity } from "./my-quantity";
export function create<T extends Quantity>(
value: number,
unit: Unit.Unit<T>,
decimalCount: number | undefined = undefined
): Amount<T> {
return Amount.create(value, unit, decimalCount);
}
export function toString<T extends Quantity>(amount: Amount<T>): string {
return Amount.toString(amount);
}
One way would be to remove the extends Quantity
from the built-in Amount
module functions. Perhaps it is enough that the Unit<T>
exists. In this case it will be possible to pass a strange quantity, such as Amount.create<"NoQuantity">(10.2, unit)
but in order to do that you need a unit
that fits with that so perhaps that is enough to avoid mistakes?
A function that accepts amounts of user defined quantities:
application.ts
import { Amount } from "uom";
import * as Units from "./my-units";
import { Temperature, Foo } from "./my-quantity";
calculate(Amount.create(10, Units.Celsius), Amount.create(11, Amount.Bar));
function calculate(t: Amount<Temperature>, f: Amount<Foo>): Amount<Foo> {
// Do something useful
}
In the Amount
module, the reason why we have T extends Quantity
instead of just T
is that Unit
is defined as Unit<T extends Quantity>
. There is no reason for the Amount
module itself to have extends Quantity
.
For example this function in Amount
:
export function create<T extends Quantity>(
value: number,
unit: Unit.Unit<T>,
decimalCount: number | undefined = undefined
): Amount<T>
In the above the important constraint is that the T
in the resulting Amount<T>
is the same as the T
in the Unit<T>
passed in. Restricting T
to Quantity
is not really important. However since the T
in Unit
is restricted, we have to add the extends Quantity
to Amount.create
.
So the question becomes if it is important to have Unit<T extends Quantity>
instead of just Unit<T>
.
Here is an example function from the Unit
module:
export function timesNumber<T extends Quantity>(
name: string,
factor: number,
unit: Unit<T>
): Unit<T> {
return transform(name, createFactorConverter(factor), unit);
}
In this case what is important is that the resulting Unit<T>
has the same T
as the one passed in. So the extends Quantity
does not do anything meaningful in this case.
I think it may boil down to that extends Quantity
restricts you from constructing an Unit
with an invalid Quantity
. So the check is only really important when constructing a UnitUnit<T>
is constructed, it will be used as parameter to other functions which have more T
that should be the same as the Unit's T
. And if the Unit is constructed with a valid T
, all other T
in functions etc. will also be valid.
And having a "valid" T in Unit<T>
is only useful if you defined a function that takes more than one quantity. For example this function will only take "Length" as a T so it is already constraining the T without any help from extends Quantity
:
function calculate(amount<Length> a)
However this function accepts any "valid" quantity:
function calculate(amount<Quantity> a)
But even in the above function T extends Quantity
is not really doing anything useful since the function specifies Quantity
and that is a union type, so the set of types that you can pass is restricted by the union type rather than the T extends Quantity
.
If we can remove T extends Quantity
we can probably also remove the built-in Quantity
union type.
There is no reason for the Amount module itself to have extends Quantity.
A valid quantity in Amount<T>
is useful when writing external code that uses it. For example, I might take a length as an argument in a function, but not care about the unit.
function isTooLong(length: Amount<"Length">): boolean {
return Amount.greaterThan(length, Amount.create(42, Units.Meter));
}
How would I express that without having Amount<T>
, where T is a quantity?
There would still be Amount<T>
where T
is a Quantity. It is only the constraint in on T
that would be removed, so instead of Amount<T extends Quantity>
there would be Amount<T>
where T
is still a Quantity. However since there is no constraint, you can pass anything as T
, but that is not really true becuase the T
would sooner or later have to be matched against the T
in a Unit<T>
anyway.
The example above would work even without the extends Quantity
constraint on T
I think?
And if you do something silly like this:
function isTooLong(length: Amount<"Foo">): boolean {
return Amount.greaterThan(length, Amount.create(42, Units.Meter));
}
It will not compile because the T
in Amount.greaterThan<T>
would not match the T
in the Unit<T>
returned by Units.Meter
.
This package is somewhat extensible because you can define new Units. However there is no clear strategy how you would package extended units and how you can use them with this package.