【问题标题】:Represent Adobe's Number-like `UnitValue` in TypeScript在 TypeScript 中表示 Adob​​e 的类似数字的 `UnitValue`
【发布时间】:2018-02-19 02:34:47
【问题描述】:

Adobe 的 ExtendScript 具有 UnitValue 类型,表示屏幕上的距离。它是一种与Number 类型相似但又不同的类型。

  1. 每个UnitValue 对象都带有一个type 字符串,例如"cm""ft"
  2. 带有不同type 值的UnitValue 对象之间的算术运算涉及隐式强制; UnitValue 对象和普通 Number 之间的算术运算按原样发生,返回 UnitValue
  3. 每个UnitValue 对象都带有许多杂项字段; UnitValue 原型实现了一堆方法。
  4. UnitValue 对象是从构造函数 - var x = UnitValue(4, "cm") 构建的。

我怎样才能最好地在 TypeScript 中表示这一点?

【问题讨论】:

  • 如何创建一个新的 UnitValue ?有构造函数吗?还是你必须调用的函数?
  • 你可以使用+ 运算符将它们加在一起吗?不幸的是,据我所知,您无法在 Typescript 中对其进行建模。

标签: typescript types numbers units-of-measurement typing


【解决方案1】:

在 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'>

但是,可以说这引入了太多的复杂性。

【讨论】:

    【解决方案2】:

    一种实现方式 - 我选择厘米作为基本单位,但您可以使用其他单位。它使用实例方法进行算术,如上所述,您不能在 TypeScript 中重载运算符。我选择维护调用方法的实例的类型(单位)而不是参数。

    查看代码中的 cmets 进行解释,但如果您有任何问题,请询问。

    export interface IUnitValue {
        add(a: IUnitValue): IUnitValue;
        subtract(a: IUnitValue): IUnitValue;
        toString(): string;
    }
    
    export type Unit = "cm" | "ft";
    
    // UnitValue is a factory function that knows how to construct different
    // types of UnitValueClass
    export const UnitValue = (value: number, unit: Unit): IUnitValue => {
        switch (unit) {
            case "cm":
                return new UnitValueClass(value, 1, unit);
            case "ft":
                return new UnitValueClass(value, 30, unit);
        }
        throw new Error(`Unrecognised unit ${unit}`);
    };
    
    export class UnitValueClass implements IUnitValue {
        private value: number;
        private cmPerUnit: number;
        private type: Unit;
    
        constructor(value: number, cmPerUnit: number, unit: Unit) {
            this.value = value;
            this.cmPerUnit = cmPerUnit;
            this.type = unit;
        }
    
        // Return the wrapped value converted to centimeters
        private toCm(): number {
            return this.value * this.cmPerUnit;
        }
    
        // When adding, convert both operands to centimeters, then convert the result
        // to the correct type and return a new UnitValue
        add(a: this): IUnitValue {
            return UnitValue((this.toCm() + a.toCm()) / this.cmPerUnit, this.type);
        }
    
        // Same logic as adding
        subtract(a: this): IUnitValue {
            return UnitValue((this.toCm() - a.toCm()) / this.cmPerUnit, this.type);
        }
    
        // Make it look pretty
        toString() {
            return `${this.value} ${this.type}`;
        }
    }
    

    这样使用:

    const a = UnitValue(45, "cm");
    const b = UnitValue(1, "ft");
    
    console.log(a.toString());            // 45 cm
    console.log(b.toString());            // 1 ft
    console.log(b.add(a).toString());     // 2.5 ft
    console.log(a.subtract(b).toString());// 15 cm
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-17
      • 2012-01-29
      • 1970-01-01
      • 2011-02-13
      • 2023-02-09
      • 2010-09-07
      • 2015-12-30
      • 1970-01-01
      相关资源
      最近更新 更多