Skip to content

Commit

Permalink
Upgrade to TypeScript 3.5 (#122)
Browse files Browse the repository at this point in the history
* Upgrade to TypeScript 3.5

* Refactor exponent handling and fix tests

* Update docs
  • Loading branch information
jscheiny authored Jul 6, 2019
1 parent 35eee07 commit 8ba2175
Show file tree
Hide file tree
Showing 29 changed files with 305 additions and 216 deletions.
21 changes: 17 additions & 4 deletions codegen/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ export interface IPartialOperatorSpec {

export interface IOperatorSpec extends ICommonSpec, IPartialOperatorSpec {}

export function getExponents({ minExponent, maxExponent }: ICommonSpec): number[] {
const exponents: number[] = [];
for (let exponent = minExponent; exponent <= maxExponent; exponent++) {
exponents.push(exponent);
export interface IExponentSpec {
value: number;
type: string;
}

export function getExponents({ minExponent, maxExponent }: ICommonSpec): IExponentSpec[] {
const exponents: IExponentSpec[] = [];
for (let value = minExponent; value <= maxExponent; value++) {
exponents.push({ value, type: `"${value}"` });
}
return exponents;
}
Expand All @@ -46,3 +51,11 @@ export function genUncurriedTypeName(spec: IOperatorSpec, left?: string | number
const args = left !== undefined && right !== undefined ? `<${left}, ${right}>` : "";
return `${spec.uncurriedTypeNamePrefix}Exponents${args}`;
}

export function genExponentName({ value }: IExponentSpec): string {
if (value === 0) {
return "Zero";
}
const sign = value < 0 ? "Neg" : "Pos";
return `${sign}${Math.abs(value)}`;
}
4 changes: 3 additions & 1 deletion codegen/genExponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { genFileHeader, getExponents, ICommonSpec } from "./common";

export function genExponentType(spec: ICommonSpec): string {
const exponents = getExponents(spec).join(" | ");
const exponents = getExponents(spec)
.map(exponent => exponent.type)
.join(" | ");
return [...genFileHeader(false), `export type Exponent = ${exponents};`, ""].join("\n");
}
29 changes: 15 additions & 14 deletions codegen/genTests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { genFileHeader, genImport, genUncurriedTypeName, getExponents, IOperatorSpec, isExponent } from "./common";
import {
genExponentName,
genFileHeader,
genImport,
genUncurriedTypeName,
getExponents,
IExponentSpec,
IOperatorSpec,
isExponent,
} from "./common";

export function genOperatorTests(spec: IOperatorSpec): string {
return [
Expand All @@ -20,18 +29,10 @@ function genTests(spec: IOperatorSpec): string[] {
return lines;
}

function genTest(spec: IOperatorSpec, left: number, right: number): string {
const result = spec.compute(left, right);
const typeName = `${spec.testTypeNamePrefix}Of${genValueName(left)}And${genValueName(right)}`;
const testType = `${genUncurriedTypeName(spec, left, right)}`;
const expectedType = isExponent(result, spec) ? `${result}` : "never";
function genTest(spec: IOperatorSpec, left: IExponentSpec, right: IExponentSpec): string {
const result = spec.compute(left.value, right.value);
const typeName = `${spec.testTypeNamePrefix}Of${genExponentName(left)}And${genExponentName(right)}`;
const testType = `${genUncurriedTypeName(spec, left.type, right.type)}`;
const expectedType = isExponent(result, spec) ? `"${result}"` : "never";
return `type ${typeName} = ${testType}; // $ExpectType ${expectedType}`;
}

function genValueName(value: number): string {
if (value === 0) {
return "Zero";
}
const sign = value < 0 ? "Neg" : "Pos";
return `${sign}${Math.abs(value)}`;
}
74 changes: 49 additions & 25 deletions codegen/genTypes.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,67 @@
import { genFileHeader, genImport, genUncurriedTypeName, getExponents, IOperatorSpec, isExponent } from "./common";
import {
genExponentName,
genFileHeader,
genImport,
genUncurriedTypeName,
getExponents,
IExponentSpec,
IOperatorSpec,
isExponent,
} from "./common";

export function genOperatorTypes(spec: IOperatorSpec): string {
const exponents = getExponents(spec);
return [
...genFileHeader(),
...genImport("Exponent", "./exponent"),
...genUncurriedType(spec, getExponents(spec)),
...genUncurriedType(spec),
...genUncurriedTable(spec, exponents),
...genAllCurriedTables(spec, exponents),
].join("\n");
}

function genUncurriedType(spec: IOperatorSpec, exponents: number[]): string[] {
const lines = [`export type ${genUncurriedTypeName(spec, "L extends Exponent", "R extends Exponent")}`];
let first = true;
function genUncurriedType(spec: IOperatorSpec): string[] {
const typeName = genUncurriedTypeName(spec, "L extends Exponent", "R extends Exponent");
const tableName = genUncurriedTableName(spec);
return [`export type ${typeName} = ${tableName}[L][R];`, ""];
}

function genUncurriedTable(spec: IOperatorSpec, exponents: IExponentSpec[]): string[] {
const name = genUncurriedTableName(spec);
const lines = [`interface ${name} {`];
for (const left of exponents) {
lines.push(indent(`${left.type}: ${genCurriedTableName(spec, left)};`));
}
lines.push("}", "");
return lines;
}

function genUncurriedTableName(spec: IOperatorSpec): string {
return `I${spec.uncurriedTypeNamePrefix}Table`;
}

function genAllCurriedTables(spec: IOperatorSpec, exponents: IExponentSpec[]): string[] {
const lines: string[] = [];
for (const left of exponents) {
const operator = first ? "=" : ":";
const prefix = indent(`${operator} L extends ${left} ?`);
first = false;
if (left in spec.specialCases) {
lines.push(`${prefix} ${spec.specialCases[left]}`);
} else {
lines.push(`${prefix}`);
lines.push(...genCurriedType(spec, exponents, left));
}
lines.push(...genCurriedTable(spec, exponents, left));
}
lines.push(indent(`: never;`));
lines.push("");
return lines;
}

function genCurriedType(spec: IOperatorSpec, exponents: number[], left: number): string[] {
const lines = ["("];
function genCurriedTable(spec: IOperatorSpec, exponents: IExponentSpec[], left: IExponentSpec): string[] {
const name = genCurriedTableName(spec, left);
const lines = [`interface ${name} {`];
for (const right of exponents) {
const result = spec.compute(left, right);
if (isExponent(result, spec)) {
lines.push(indent(`R extends ${right} ? ${result} :`));
}
const result = spec.compute(left.value, right.value);
const value = isExponent(result, spec) ? `"${result}"` : "never";
lines.push(indent(`${right.type}: ${value};`));
}
lines.push(indent("never"));
lines.push(")");
return lines.map(indent).map(indent);
lines.push("}", "");
return lines;
}

function genCurriedTableName(spec: IOperatorSpec, left: IExponentSpec): string {
return `I${spec.curriedTypeNamePrefix}${genExponentName(left)}Table`;
}

function indent(line: string): string {
Expand Down
2 changes: 1 addition & 1 deletion docs/builtin.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The built-in units include the standard set of SI base dimension and correspondi

* `Length` / `meters`
* `Mass` / `kilograms`
* `Time` `seconds`
* `Time` / `seconds`
* `ElectricCurrent` / `amperes`
* `Temperature` / `kelvin`
* `AmountOfSubstance` / `moles`
Expand Down
2 changes: 1 addition & 1 deletion docs/defining-quantities.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Defining Quantities

We define a **quantity** as the type of a measurement. Length, time, force, velocity, and pressure are all examples of quantities. Safe Units contains many built in quantities so we can write unit safe code easily:
We define a **quantity** as the type of a measurement. Length, time, force, velocity, and pressure are all examples of quantities. Safe Units contains many built in quantities so we can easily write unit safe code:

```ts
import { Acceleration, Time, Velocity } from "safe-units";
Expand Down
4 changes: 2 additions & 2 deletions docs/generic-measures.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type Measure<U extends Unit> = IGenericMeasure<number, U>;
After we've defined the type of `WrappedMeasure` we now define the class itself by calling `createMeasureType`. This function takes an object which let's the generic measure type know how to perform operations on our numeric type. Note that for this simple example, we generally just unwrap the value, perform the arithmetic operation and then wrap it back up. Most of these operations should be self-explanatory, however some require some further explanation:

- `one`: A function with no arguments that simply returns the 1 value or multiplicative identity of your number system. This is used to construct base units whose values are implicitly one.
- `pow`: This function is slightly different from the rest of the arithmetic operations in that it doesn't take to values of type `N`, instead its signature is: `pow: (base: N, power: Exponent) => N` where `Exponent` is the union of `-5 | -4 | ... | 4 | 5`. This is due to the computational limitations of the library that we need to be specific in the kinds of exponents we can handle.
- `pow`: This function is slightly different from the rest of the arithmetic operations in that it doesn't take to values of type `N`, instead its signature is: `pow: (base: N, power: number) => N`. The `power` argument is a JavaScript `number` but will only ever be an integer between -5 and 5, inclusive. This is due to the computational limitations of the library that we need to be specific in the kinds of exponents we can handle.
- `compare`: A function that returns a negative `number` if its first argument is less than its second, a positive `number` if its first argument is greater than its second, and `0` if the arguments are equal.

## Usage
Expand All @@ -69,7 +69,7 @@ By default, generic measures come with a set of static methods that can be appli

```ts
declare function foo(value: WrappedNumber): WrappedNumber;
declare const mass: WrappedMeasure<{ mass: 1 }>;
declare const mass: WrappedMeasure<{ mass: "1" }>;

const WrappedMeasure = createMeasureType({ ... }, {
foo: wrapUnaryFn(foo),
Expand Down
8 changes: 4 additions & 4 deletions docs/limitations.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Limitations

Since Safe Units is typesafe, it must compute units at compile time. Due to some technical limitations of what you can do with types in TypeScript, there is one major limitation to this library: The exponents for dimensions of your units are limited to integers between -5 and 5 (inclusive). This means that you can not represent a value of 30 m<sup>6</sup> in this library (though, why would you?).
Since Safe Units is typesafe, it must compute units at compile time. Due to some technical limitations of what you can do with types in TypeScript, there is one major limitation to this library: The exponents for dimensions of your units are limited to integers between -5 and 5, inclusive. (However, under the hood these are represented by string literals between `"-5"` and `"5"`). This means that you can not represent a value of 30 m<sup>6</sup> in this library (though, why would you?).

In the research I've conducted for this library I cannot find any instances in which it would be useful to use units with such extreme exponents. If you're aware of any such use cases, please [file an issue](https://github.com/jscheiny/safe-units/issues/new) to discuss it.

Expand All @@ -12,7 +12,7 @@ If two units will multiply or divide together to create an exponent out of range

```ts
const a = Measure.of(1, meters.cubed());
const b = Measure.of(1, meters.toThe(-3));
const b = Measure.of(1, meters.toThe("-3"));
const product = a.times(a);
// ~ Error: The result would have unit m^6
const quotient = a.over(b);
Expand All @@ -29,6 +29,6 @@ const a = m.squared();
// ~~~~~~~~~~~ Error: squared has type never
const b = m.cubed();
// ~~~~~~~~~ Error: cubed has type never
const c = m.toThe(3);
// ~ Error: m cannot be cubed so 3 is an invalid argument
const c = m.toThe("3");
// ~~~ Error: m cannot be cubed so "3" is an invalid argument
```
18 changes: 10 additions & 8 deletions docs/measures.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,13 @@ const doubledLong = t.times(Measure.dimensionless(2));
Measure<U>.toThe<E>(exponent: E): Measure<ExponentiateUnit<U, E>>
Measure.pow<U, E>(measure: Measure<U>, exponent: E): Measure<ExponentiateUnit<U, E>>

Measure<U>.squared(): Measure<ExponentiateUnit<U, 2>>
Measure<U>.cubed(): Measure<ExponentiateUnit<U, 2>>
Measure<U>.squared(): Measure<ExponentiateUnit<U, "2">>
Measure<U>.cubed(): Measure<ExponentiateUnit<U, "3">>
```

The first two methods raise a measure's value and unit to a given exponent (within a limited range). The last two methods, `squared` and `cubed`, are convenience methods for `measure.toThe(2)` and `measure.toThe(3)` respectively.
The first two methods raise a measure's value and unit to a given exponent (within a limited range). The last two methods, `squared` and `cubed`, are convenience methods for `measure.toThe("2")` and `measure.toThe("3")` respectively.

Note that the exponents passed in are string literals and not numbers.

*Examples:*

Expand All @@ -213,19 +215,19 @@ const side = Measure.of(10, meters);
const area: Area = side.squared(); // 100 m^2
const volume: Volume = side.cubed(); // 1000 m^3

const s: Length = volume.toThe(-3); // 10 m
const s: Length = volume.toThe("-3"); // 10 m
```

**Note:** There are limitations on what measures you may exponentiate. See [Limitations](Limitations).

### Reciprocals

```ts
Measure<U>.inverse(): Measure<ExponentiateUnit<U, -1>>
Measure<U>.reciprocal(): Measure<ExponentiateUnit<U, -1>>
Measure<U>.inverse(): Measure<ExponentiateUnit<U, "-1">>
Measure<U>.reciprocal(): Measure<ExponentiateUnit<U, "-1">>
```

Computes the reciprocal of the value and unit of the measure. Both methods are identical and equivalent to `measure.toThe(-1)`.
Computes the reciprocal of the value and unit of the measure. Both methods are identical and equivalent to `measure.toThe("-1")`.

*Examples:*

Expand Down Expand Up @@ -357,7 +359,7 @@ Measure<U>.unsafeMap<V>(
): Measure<V>;
```

If only one argument is passed, performs a mapping on the value of a measure without affecting the unit of the measure. If both arguments are passed maps both the unit and value of a measure. This is generally used for internal purposes and should be avoided when possible. Instead consider using a [function wrapper](#function-wrappers).
If only one argument is passed, performs a mapping on the value of a measure without affecting the unit of the measure. If both arguments are passed maps both the unit and value of a measure. This is generally used for internal purposes and should be avoided whenever possible. Instead consider using a [function wrapper](#function-wrappers).

## Representation

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@
"rimraf": "^2.6.3",
"ts-jest": "^22.4.6",
"ts-node": "^6.0.3",
"tslint": "^5.10.0",
"tslint": "^5.18.0",
"tslint-config-prettier": "^1.12.0",
"tslint-config-standard": "^7.0.0",
"tslint-no-circular-imports": "^0.4.0",
"tslint-plugin-prettier": "^1.3.0",
"typescript": "~2.9.1",
"typescript": "~3.5.2",
"typestyle": "^2.0.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export type AddendOf<N extends Exponent> = SubtractExponents<AddExponents<Expone
export type SubtrahendOf<N extends Exponent> = AddendOf<Negative<N>>;

/** Exponents that can be multiplied with N without producing an error. */
export type MultiplicandOf<N extends Exponent> = 0 extends N ? Exponent : DivideExponents<Exponent, ProductOf<N>>;
export type MultiplicandOf<N extends Exponent> = "0" extends N ? Exponent : DivideExponents<Exponent, ProductOf<N>>;

/** Exponents that are non-error multiples of N. */
export type ProductOf<N extends Exponent> = MultiplyExponents<Exponent, N>;

type Negative<N extends Exponent> = MultiplyExponents<N, -1>;
type Negative<N extends Exponent> = MultiplyExponents<N, "-1">;
20 changes: 20 additions & 0 deletions src/exponent/exponentValueArithmetic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Exponent } from "./generated/exponent";

export function getExponentValue(value: Exponent): number {
return parseInt(value, 10);
}

export const negateExponent = (value: Exponent) => toExponent(-getExponentValue(value));
export const addExponents = wrapBinaryExponentFn((left, right) => left + right);
export const multiplyExponents = wrapBinaryExponentFn((left, right) => left * right);
export const divideExponents = wrapBinaryExponentFn((left, right) => left / right);

type BinaryExponentFn = (left: Exponent, right: Exponent) => Exponent;

function wrapBinaryExponentFn(fn: (left: number, right: number) => number): BinaryExponentFn {
return (left, right) => toExponent(fn(getExponentValue(left), getExponentValue(right)));
}

function toExponent(value: number): Exponent {
return `${value}` as Exponent;
}
3 changes: 2 additions & 1 deletion src/exponent/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./exponentArithmetic";
export * from "./exponentTypeArithmetic";
export * from "./exponentValueArithmetic";
export * from "./generated/addition";
export * from "./generated/division";
export * from "./generated/exponent";
Expand Down
12 changes: 6 additions & 6 deletions src/measure/__test__/numberMeasureTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("Number measures", () => {

describe("dimension", () => {
it("should create dimensions with value 1", () => {
expect(Measure.dimension("foo", "f")).toEqual({ value: 1, unit: { foo: ["f", 1] }, symbol: "f" });
expect(Measure.dimension("foo", "f")).toEqual({ value: 1, unit: { foo: ["f", "1"] }, symbol: "f" });
});
});

Expand Down Expand Up @@ -96,7 +96,7 @@ describe("Number measures", () => {
});

it("pow", () => {
expect(Measure.pow(Measure.of(3, meters), 4)).toEqual(Measure.of(81, meters.toThe(4)));
expect(Measure.pow(Measure.of(3, meters), "4")).toEqual(Measure.of(81, meters.toThe("4")));
});

it("round", () => {
Expand Down Expand Up @@ -160,8 +160,8 @@ describe("Number measures", () => {

expect(value.inverse()).toEqual(Measure.of(0.1, meters.inverse()));
expect(value.reciprocal()).toEqual(Measure.of(0.1, meters.inverse()));
expect(value.toThe(0)).toEqual(Measure.dimensionless(1));
expect(value.toThe(1)).toEqual(Measure.of(10, meters));
expect(value.toThe("0")).toEqual(Measure.dimensionless(1));
expect(value.toThe("1")).toEqual(Measure.of(10, meters));
expect(value.squared()).toEqual(Measure.of(100, meters.squared()));
expect(value.cubed()).toEqual(Measure.of(1000, meters.cubed()));
});
Expand Down Expand Up @@ -255,8 +255,8 @@ describe("Number measures", () => {

it("should format units with only negative exponents", () => {
expectFormat(seconds.inverse(), "1 s^-1");
expectFormat(seconds.toThe(-2), "1 s^-2");
expectFormat(seconds.toThe(-2).times(meters.toThe(-3)), "1 m^-3 * s^-2");
expectFormat(seconds.toThe("-2"), "1 s^-2");
expectFormat(seconds.toThe("-2").times(meters.toThe("-3")), "1 m^-3 * s^-2");
});

it("should format units with positive exponents and one negative exponent", () => {
Expand Down
Loading

0 comments on commit 8ba2175

Please sign in to comment.