From 1b4d0553cc9ed9ec0ef915ac655d7337c8431b67 Mon Sep 17 00:00:00 2001 From: Harper Date: Tue, 10 Jun 2025 13:53:24 -0500 Subject: [PATCH 1/3] Extract tsconfig.src.base.json from tsconfig.base.json `tsconfig.base.json` includes settings that apply to ALL typescript in the project (including tests and docsgen), while `tsconfig.src.base.json` includes settings that should only apply to the source code of the main library. This fixes the `include` and `exclude` fields intended for library compilation being applied to the `tests` and `docsgen` folders, which made those folders actually just compile the library instead of their actual code. --- tsconfig.base.json | 12 ++---------- tsconfig.cjs.json | 4 ++-- tsconfig.esm.json | 4 ++-- tsconfig.src.base.json | 11 +++++++++++ 4 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 tsconfig.src.base.json diff --git a/tsconfig.base.json b/tsconfig.base.json index 971b37f..47744a0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -19,13 +19,5 @@ "es2015", "es2015.core" ] - }, - "include": [ - "src" - ], - "exclude": [ - "dist", - "node_modules", - "test" - ] -} \ No newline at end of file + } +} diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 8411f0d..5474a45 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,8 +1,8 @@ { - "extends": "./tsconfig.base.json", + "extends": "./tsconfig.src.base.json", "compilerOptions": { "module": "commonjs", "outDir": "./dist/cjs", "target": "ES2015" } -} \ No newline at end of file +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json index d6aae5e..e7a334c 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -1,8 +1,8 @@ { - "extends": "./tsconfig.base.json", + "extends": "./tsconfig.src.base.json", "compilerOptions": { "module": "es6", "outDir": "./dist/esm", "target": "es6" } -} \ No newline at end of file +} diff --git a/tsconfig.src.base.json b/tsconfig.src.base.json new file mode 100644 index 0000000..452e67c --- /dev/null +++ b/tsconfig.src.base.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "include": [ + "src" + ], + "exclude": [ + "dist", + "node_modules", + "test" + ] +} From d84ddfe32cc57c219559ce9aa7b04eacfb79c82a Mon Sep 17 00:00:00 2001 From: Harper Date: Tue, 10 Jun 2025 12:24:34 -0500 Subject: [PATCH 2/3] Make Basis type parameters type-safe Before, the type system would let you pass in anything as a basis, even things that weren't objects. Now, Basis types must conform to the type Record (aliased as BasisType, with a doc comment). I had to change how the errorBasis test works (it's simpler now). This is a breaking change for anyone wrapping `Measure`. --- docs/examples/genericMeasureWrapped.ts | 4 ++-- docs/examples/genericMeasuresStaticMethods.ts | 4 ++-- docs/generic-measures.md | 4 ++-- src/measure/format.ts | 4 ++-- src/measure/genericMeasure.ts | 6 +++--- src/measure/genericMeasureClass.ts | 6 +++--- src/measure/genericMeasureFactory.ts | 11 +++++++---- src/measure/genericMeasureStatic.ts | 5 +++-- src/measure/genericMeasureUtils.ts | 9 +++++---- src/measure/index.ts | 2 +- src/measure/numberMeasure.ts | 3 ++- src/measure/unitSystem.ts | 12 +++++++++--- test/measureTypeTests.ts | 2 +- 13 files changed, 42 insertions(+), 30 deletions(-) diff --git a/docs/examples/genericMeasureWrapped.ts b/docs/examples/genericMeasureWrapped.ts index 2d03e58..21f99b4 100644 --- a/docs/examples/genericMeasureWrapped.ts +++ b/docs/examples/genericMeasureWrapped.ts @@ -1,9 +1,9 @@ import { WrappedNumber, wrap } from "./genericMeasureIntro"; // START -import { createMeasureType, GenericMeasure, Unit } from "safe-units"; +import { type BasisType, createMeasureType, GenericMeasure, Unit } from "safe-units"; -type WrappedMeasure> = GenericMeasure; +type WrappedMeasure> = GenericMeasure; const WrappedMeasure = createMeasureType({ one: () => wrap(1), neg: x => wrap(-x.value), diff --git a/docs/examples/genericMeasuresStaticMethods.ts b/docs/examples/genericMeasuresStaticMethods.ts index 512fdd1..cc826fd 100644 --- a/docs/examples/genericMeasuresStaticMethods.ts +++ b/docs/examples/genericMeasuresStaticMethods.ts @@ -1,7 +1,7 @@ -import { GenericMeasure, Mass, NumericOperations, Unit, createMeasureType, wrapUnaryFn } from "safe-units"; +import { type BasisType, GenericMeasure, Mass, NumericOperations, Unit, createMeasureType, wrapUnaryFn } from "safe-units"; class WrappedNumber {} -type WrappedMeasure> = GenericMeasure; +type WrappedMeasure> = GenericMeasure; declare const numericOperations: NumericOperations; // START diff --git a/docs/generic-measures.md b/docs/generic-measures.md index 11d53a9..6b85f85 100644 --- a/docs/generic-measures.md +++ b/docs/generic-measures.md @@ -23,13 +23,13 @@ We can then use this class just as we would use `Measure`, except anywhere we'd Let's deconstruct this example to explain what's going on. First we start with this type definition: ```ts -type WrappedMeasure> = GenericMeasure; +type WrappedMeasure> = GenericMeasure; ``` This line isn't strictly necessary, but it is often useful to have our `WrappedMeasure` available as a type. Having a type for the measure is useful for writing generic functions on wrapped measures. All this line does is bind the numeric type of `GenericMeasure`. Similarly, the `Measure` type has the following definition: ```ts -type Measure> = GenericMeasure; +type Measure> = GenericMeasure; ``` 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: diff --git a/src/measure/format.ts b/src/measure/format.ts index adc1f82..300f8e9 100644 --- a/src/measure/format.ts +++ b/src/measure/format.ts @@ -1,9 +1,9 @@ -import { UnitSystem } from "./unitSystem"; +import { type BasisType, UnitSystem } from "./unitSystem"; import { Unit } from "./unitTypeArithmetic"; type SymbolAndExponent = [symbol: string, exponent: number]; -export function defaultFormatUnit(unit: Unit, unitSystem: UnitSystem): string { +export function defaultFormatUnit(unit: Unit, unitSystem: UnitSystem): string { const positive: SymbolAndExponent[] = []; const negative: SymbolAndExponent[] = []; unitSystem.getDimensions().forEach(dimension => { diff --git a/src/measure/genericMeasure.ts b/src/measure/genericMeasure.ts index 2798bef..a5dd84b 100644 --- a/src/measure/genericMeasure.ts +++ b/src/measure/genericMeasure.ts @@ -1,9 +1,9 @@ -import { UnitSystem } from "./unitSystem"; +import { type BasisType, UnitSystem } from "./unitSystem"; import { CubeUnit, DivideUnits, MultiplyUnits, ReciprocalUnit, SquareUnit, Unit } from "./unitTypeArithmetic"; export interface MeasureFormatter { formatValue?: (value: N) => string; - formatUnit?: (unit: Unit, unitSystem: UnitSystem) => string; + formatUnit?: (unit: Unit, unitSystem: UnitSystem) => string; } /** The set of numeric operations required to fully represent a `GenericMeasure` for a given numeric type */ @@ -29,7 +29,7 @@ export interface NumericOperations { } /** A numeric value with a corresponding unit of measurement. */ -export interface GenericMeasure> { +export interface GenericMeasure> { /** The numeric value of this measure */ readonly value: N; /** The unit of this measure */ diff --git a/src/measure/genericMeasureClass.ts b/src/measure/genericMeasureClass.ts index b822d2a..2fff538 100644 --- a/src/measure/genericMeasureClass.ts +++ b/src/measure/genericMeasureClass.ts @@ -1,10 +1,10 @@ import { defaultFormatUnit } from "./format"; import { GenericMeasure, MeasureFormatter, NumericOperations } from "./genericMeasure"; -import { UnitSystem } from "./unitSystem"; +import { type BasisType, UnitSystem } from "./unitSystem"; import { DivideUnits, MultiplyUnits, ReciprocalUnit, SquareUnit, Unit, CubeUnit } from "./unitTypeArithmetic"; interface GenericMeasureClass { - createMeasure: >( + createMeasure: >( value: N, unit: U, unitSystem: UnitSystem, @@ -28,7 +28,7 @@ export function createMeasureClass(num: NumericOperations): GenericMeasure } } - class Measure> implements GenericMeasure { + class Measure> implements GenericMeasure { constructor( public readonly value: N, public readonly unit: U, diff --git a/src/measure/genericMeasureFactory.ts b/src/measure/genericMeasureFactory.ts index 278395b..a3322f1 100644 --- a/src/measure/genericMeasureFactory.ts +++ b/src/measure/genericMeasureFactory.ts @@ -1,7 +1,7 @@ import { GenericMeasure, NumericOperations } from "./genericMeasure"; import { createMeasureClass } from "./genericMeasureClass"; import { GenericMeasureStatic, getGenericMeasureStaticMethods } from "./genericMeasureStatic"; -import { UnitSystem } from "./unitSystem"; +import { type BasisType, UnitSystem } from "./unitSystem"; import { DimensionUnit, DimensionlessUnit, Unit } from "./unitTypeArithmetic"; /** The functions needed to construct a measure of a given numeric type */ @@ -16,7 +16,7 @@ interface GenericMeasureFactory { * @param symbol the symbol of the base unit of the dimension (e.g. "m") * @returns A measure representing 1 base unit of the dimension (1 m) */ - dimension( + dimension( unitSystem: UnitSystem, dimension: Dimension, symbol?: string, @@ -28,7 +28,10 @@ interface GenericMeasureFactory { * @param value the value of the measure * @returns a measure with no dimensions */ - dimensionless(unitSystem: UnitSystem, value: N): GenericMeasure>; + dimensionless( + unitSystem: UnitSystem, + value: N, + ): GenericMeasure>; /** * Creates a measure as a multiple of another measure. @@ -37,7 +40,7 @@ interface GenericMeasureFactory { * @param symbol an optional unit symbol for this measure * @returns a measure of value number of quantities. */ - of>( + of>( value: N, quantity: GenericMeasure, symbol?: string, diff --git a/src/measure/genericMeasureStatic.ts b/src/measure/genericMeasureStatic.ts index eaa6113..831a521 100644 --- a/src/measure/genericMeasureStatic.ts +++ b/src/measure/genericMeasureStatic.ts @@ -1,5 +1,6 @@ import { GenericMeasure, NumericOperations } from "./genericMeasure"; import { BinaryFn, PrefixFn, SpreadFn, wrapBinaryFn, wrapReducerFn } from "./genericMeasureUtils"; +import type { BasisType } from "./unitSystem"; import { DivideUnits, MultiplyUnits, Unit } from "./unitTypeArithmetic"; export interface GenericMeasureStatic { @@ -19,13 +20,13 @@ export interface GenericMeasureStatic { subtract: BinaryFn; /** Static version of `left.times(right)` */ - multiply, Right extends Unit>( + multiply, Right extends Unit>( left: GenericMeasure, right: GenericMeasure, ): GenericMeasure>; /** Static version of `left.div(right)` */ - divide, Right extends Unit>( + divide, Right extends Unit>( left: GenericMeasure, right: GenericMeasure, ): GenericMeasure>; diff --git a/src/measure/genericMeasureUtils.ts b/src/measure/genericMeasureUtils.ts index 717af19..59994ba 100644 --- a/src/measure/genericMeasureUtils.ts +++ b/src/measure/genericMeasureUtils.ts @@ -1,24 +1,25 @@ import { GenericMeasure } from "./genericMeasure"; +import type { BasisType } from "./unitSystem"; import { Unit } from "./unitTypeArithmetic"; /** A function which applies a symbol prefix and multiplier to a given measure. */ -export type PrefixFn = >( +export type PrefixFn = >( measure: GenericMeasure, ) => GenericMeasure; /** A function which transforms a single measure into another measure with the same unit. */ -export type UnaryFn = >( +export type UnaryFn = >( x: GenericMeasure, ) => GenericMeasure; /** A function which transforms two measures with same unit into a single measure with the same unit. */ -export type BinaryFn = >( +export type BinaryFn = >( left: GenericMeasure, right: GenericMeasure, ) => GenericMeasure; /** A function which transforms one or more measure with the same unit into a single measure with the same unit. */ -export type SpreadFn = >( +export type SpreadFn = >( first: GenericMeasure, ...rest: Array> ) => GenericMeasure; diff --git a/src/measure/index.ts b/src/measure/index.ts index 1b5adeb..15ab66e 100644 --- a/src/measure/index.ts +++ b/src/measure/index.ts @@ -11,7 +11,7 @@ export { wrapUnaryFn, } from "./genericMeasureUtils"; export { Measure } from "./numberMeasure"; -export { UnitSystem } from "./unitSystem"; +export { BasisType, UnitSystem } from "./unitSystem"; export { CubeUnit, DimensionlessUnit, diff --git a/src/measure/numberMeasure.ts b/src/measure/numberMeasure.ts index a68f1a3..3447dff 100644 --- a/src/measure/numberMeasure.ts +++ b/src/measure/numberMeasure.ts @@ -1,6 +1,7 @@ import { GenericMeasure, NumericOperations } from "./genericMeasure"; import { createMeasureType, GenericMeasureType } from "./genericMeasureFactory"; import { SpreadFn, UnaryFn, wrapSpreadFn, wrapUnaryFn } from "./genericMeasureUtils"; +import type { BasisType } from "./unitSystem"; import { Unit } from "./unitTypeArithmetic"; interface MeasureStaticMethods { @@ -35,5 +36,5 @@ const numericOps: NumericOperations = { format: x => `${x}`, }; -export type Measure> = GenericMeasure; +export type Measure> = GenericMeasure; export const Measure: GenericMeasureType = createMeasureType(numericOps, staticMethods); diff --git a/src/measure/unitSystem.ts b/src/measure/unitSystem.ts index ee3161e..ffb67c7 100644 --- a/src/measure/unitSystem.ts +++ b/src/measure/unitSystem.ts @@ -7,7 +7,13 @@ import { Unit, } from "./unitTypeArithmetic"; -type UnitSystemResult = [Basis[keyof Basis]] extends [string] +/** + * The keys of a type system's basis are the names of the dimensions, and the values are the symbols + * of each dimension's base unit. + */ +export type BasisType = Record; + +type UnitSystemResult = [Basis[keyof Basis]] extends [string] ? UnitSystem : `Dimension '${NonStringValuedKeys}' does not have a valid symbol`; @@ -20,7 +26,7 @@ type NonStringValuedKeys = keyof { * are defined by the keys of the Basis type parameter. The base units are given by the symbol map passed into the * constructor. */ -export class UnitSystem implements UnitSystem { +export class UnitSystem implements UnitSystem { private readonly dimensions: Array; /** @@ -48,7 +54,7 @@ export class UnitSystem implements UnitSystem { * ... * }); */ - public static from(symbols: Basis): UnitSystemResult { + public static from(symbols: Basis): UnitSystemResult { return new UnitSystem(symbols) as UnitSystemResult; } diff --git a/test/measureTypeTests.ts b/test/measureTypeTests.ts index f034740..71688ff 100644 --- a/test/measureTypeTests.ts +++ b/test/measureTypeTests.ts @@ -84,5 +84,5 @@ const validUnitSystem = UnitSystem.from({ length: "m", mass: "kg", time: "s" } a expectTrue(value(validUnitSystem).hasType>()); const errorBasis = { length: "m", mass: 3, time: "kg" }; +// @ts-expect-error errorBasis is not a valid basis for a type system const errorUnitSystem = UnitSystem.from(errorBasis); -expectTrue(value(errorUnitSystem).hasType<"Dimension 'mass' does not have a valid symbol">()); From cbc9d005840741b99ca87532ca12af2ecb214fc5 Mon Sep 17 00:00:00 2001 From: Harper Date: Tue, 10 Jun 2025 14:43:57 -0500 Subject: [PATCH 3/3] Make prettier happy --- docs/examples/genericMeasuresStaticMethods.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/examples/genericMeasuresStaticMethods.ts b/docs/examples/genericMeasuresStaticMethods.ts index cc826fd..a3bd2ef 100644 --- a/docs/examples/genericMeasuresStaticMethods.ts +++ b/docs/examples/genericMeasuresStaticMethods.ts @@ -1,4 +1,12 @@ -import { type BasisType, GenericMeasure, Mass, NumericOperations, Unit, createMeasureType, wrapUnaryFn } from "safe-units"; +import { + type BasisType, + GenericMeasure, + Mass, + NumericOperations, + Unit, + createMeasureType, + wrapUnaryFn, +} from "safe-units"; class WrappedNumber {} type WrappedMeasure> = GenericMeasure;