在 TypeScript 中使用泛型和文字类型实现单元在技术上是可行的:
// union of all possible unit types
type UnitType = 'cm' | 'm';
interface UnitConversion<From extends UnitType, To extends UnitType> {
from: From;
to: To;
convert(value: UnitValue<From>): UnitValue<To>;
}
function conversion<From extends UnitType, To extends UnitType>(
from: From, to: To, convert: (value: UnitValue<From>) => UnitValue<To>
): UnitConversion<From, To> {
return { from, to, convert };
}
function identity<T extends UnitType>(t: T): UnitConversion<T, T> {
return { from: t, to: t, convert: v => v };
}
// conversion table for each pair of unit types
const IMPLICIT_CONVERSIONS = {
'cm': {
'cm': identity('cm'),
'm': conversion('cm', 'm', v => new UnitValue(v.value * 0.1, 'm')),
},
'm': {
'cm': conversion('m', 'm', v => new UnitValue(v.value * 10, 'cm')),
'm': identity('m'),
},
};
type ImplicitConversions<
Left extends UnitType,
Right extends UnitType
> = (typeof IMPLICIT_CONVERSIONS)[Left][Right]['to'];
function convert(conversion: UnitConversion<any, any>, value: UnitValue<any>) {
return value.type === conversion.to ? value : conversion.convert(value);
}
type UnitPair<T extends UnitType> = {
left: UnitValue<T>;
right: UnitValue<T>;
};
function convertToCommonType<Left extends UnitType, Right extends UnitType>(
left: UnitValue<Left>,
right: UnitValue<Right>
): UnitPair<ImplicitConversions<Left, Right>> {
const conversion = IMPLICIT_CONVERSIONS[left.type][right.type];
return { left: convert(conversion, left), right: convert(conversion, right) };
}
class UnitValue<Type extends UnitType> {
constructor(
readonly value: number,
readonly type: Type,
) { }
/** Type-safe unit addition */
add<T extends UnitType>(value: UnitValue<T>): UnitValue<ImplicitConversions<Type, T>> {
const { left, right } = convertToCommonType(this, value);
return new UnitValue(left.value + right.value, left.type);
}
}
然后像这样使用它:
const common = convertToCommonType(
new UnitValue(3, 'cm'),
new UnitValue(10, 'm')
);
// => result type: UnitValue<'m'>
const z = new UnitValue(4, 'cm').add(new UnitValue(5, 'm'));
// => result type: UnitValue<'m'>
但是,可以说这引入了太多的复杂性。