有没有办法在TypeScript中创建扩展基本类型的名义类型?

时间:2014-11-07 21:45:00

标签: typescript

我说我有两种类型的数字,例如latitudelongitude。我想用基本number原语来表示这些变量,但不允许在打字稿中将longitude分配给latitude变量。

有没有办法对number原语进行子类化,以便打字稿将此分配检测为非法?在某种程度上强制进行名义输入以使这段代码失败?

var longitude : LongitudeNumber = new LongitudeNumber();
var latitude : LatitudeNumber;
latitude = longitude; // <-- type failure

"How to extend a primitive type in typescript?"的答案似乎会让我朝着正确的方向前进,但我不知道如何扩展该解决方案,为不同的kinds数字创建不同的名义子类型。< / p>

我必须包装原语吗?如果是这样,我可以使它表现得像普通数字一样无缝,还是我必须引用一个子成员?我可以以某种方式创建一个typescript编译时数字子类吗?

6 个答案:

答案 0 :(得分:10)

以下是实现此目的的简单方法:

要求

您只需要两个功能,一个用于将数字转换为数字类型,另一个用于反向处理。以下是两个功能:

module NumberType {
    /**
     * Use this function to convert to a number type from a number primitive.
     * @param n a number primitive
     * @returns a number type that represents the number primitive
     */
    export function to<T extends Number>(n : number) : T {
        return (<any> n);
    }

    /**
     * Use this function to convert a number type back to a number primitive.
     * @param nt a number type
     * @returns the number primitive that is represented by the number type
     */
    export function from<T extends Number>(nt : T) : number {
        return (<any> nt);
    }
}

用法

您可以像这样创建自己的数字类型:

interface LatitudeNumber extends Number {
    // some property to structurally differentiate MyIdentifier
    // from other number types is needed due to typescript's structural
    // typing. Since this is an interface I suggest you reuse the name
    // of the interface, like so:
    LatitudeNumber;
}

以下是如何使用LatitudeNumber的示例

function doArithmeticAndLog(lat : LatitudeNumber) {
    console.log(NumberType.from(lat) * 2);
}

doArithmeticAndLog(NumberType.to<LatitudeNumber>(100));

这会将200记录到控制台。

正如您所期望的那样,无法使用数字原语或其他数字类型调用此函数:

interface LongitudeNumber extends Number {
    LongitudeNumber;
}

doArithmeticAndLog(2); // compile error: (number != LongitudeNumber)
doArithmeticAndLog(NumberType.to<LongitudeNumber>(2)); // compile error: LongitudeNumer != LatitudeNumber

工作原理

这样做只是傻瓜打字稿相信一个原始数字实际上是数字接口的一些扩展(我称之为数字类型),而实际上原始数字永远不会转换为实现数字类型的实际对象。转换不是必需的,因为数字类型的行为类似于原始数字类型;数字类型只是一个数字原语。

技巧只是转换为any,因此打字稿会停止类型检查。所以上面的代码可以重写为:

function doArithmeticAndLog(lat : LatitudeNumber) {
    console.log(<any> lat * 2);
}

doArithmeticAndLog(<any>100);

正如您所看到的,函数调用甚至不是必需的,因为数字及其数字类型可以互换使用。这意味着在运行时需要绝对零性能或内存丢失。我仍然强烈建议使用函数调用,因为函数调用的成本几乎为零,并且通过自己强制转换为any,你会松散类型安全(例如doArithmeticAndLog(<any>'bla')将编译,但会导致NaN在运行时登录到控制台)...但是如果你想要完整的性能,你可以使用这个技巧。

它也可以用于其他原语,如字符串和布尔值。

快乐打字!

答案 1 :(得分:3)

没有办法做到这一点。

在GitHub网站上跟踪此问题的建议是Units of Measure

在将来的版本中,您将能够使用type来定义基元的备用名称,但这些名称不会与它们相关联:

type lat = number;
type lon = number;
var x: lat = 43;
var y: lon = 48;
y = 'hello'; // error
x = y; // No error

答案 2 :(得分:3)

实际上有一种方法可以实现您想要实现的目标,但它有点棘手,有一些限制,对于第一次看到该代码的人来说可能完全不合理,所以将其视为一个好奇而不是实际的实施;)

好的,让我们走吧。首先,我们需要创建一个&#34;子类&#34; Number。问题是,lib.d.ts实际上将Number声明为接口,而不是类(这是合理的 - 不需要实现方法,浏览器负责处理)。所以我们必须实现接口声明的所有方法,幸好我们可以使用声明的var Number的现有实现。

class WrappedNumber implements Number {
    //this will serve as a storage for actual number
    private value: number;

    constructor(arg?: number) {
        this.value = arg;
    }

    //and these are the methods needed by Number interface
    toString(radix?: number): string {
        return Number.prototype.toString.apply(this.value, arguments);
    }

    toFixed(fractionDigits?: number): string {
        return Number.prototype.toFixed.apply(this.value, arguments);
    }

    toExponential(fractionDigits?: number): string {
        return Number.prototype.toExponential.apply(this.value, arguments);
    }

    toPrecision(precision: number): string {
        return Number.prototype.toPrecision.apply(this.value, arguments);
    }

    //this method isn't actually declared by Number interface but it can be useful - we'll get to that
    valueOf(): number {
        return this.value;
    }
}

你去了,我们创建了一个类型WrappedNumber,其行为就像数字类型一样。您甚至可以添加两个WrappedNumber - 感谢valueOf()方法。但是,这里有两个限制:首先,您需要转换变量来执行此操作。第二:结果将是常规number,因此它应该在之后再次包装。让我们看一下加法的例子。

var x = new WrappedNumber(5);
var y = new WrappedNumber(7);

//We need to cast x and y to <any>, otherwise compiler
//won't allow you to add them
var z = <any>x + <any>y;

//Also, compiler now recognizes z as of type any.
//During runtime z would be a regular number, as
//we added two numbers. So instead, we have to wrap it again
var z = new WrappedNumber(<any>x + <any>y); //z is a WrappedNumber, which holds value 12 under the hood

在我看来,这是最棘手的部分。我们现在创建了两个类LatitudeLongitude,它们将继承自WrappedNumber(因此它们表现为数字)

class Latitude extends WrappedNumber {
    private $;
}
class Longitude extends WrappedNumber {
    private $;
}

到底是什么?嗯,TypeScript在比较类型时使用duck typing。这意味着两种不同类型被认为是&#34;兼容&#34; (因此,当它们具有相同的属性集时,可以赋予它们自己,即你可以将一种类型的变量赋值给另一种类型的值)。解决方案非常简单:添加私有成员。这个私人成员是纯粹的虚拟成员,它不会在任何地方使用,也不会被编译。但它使TypeScript认为LatitudeLongitude是完全不同的类型,而且我们感兴趣的更多,不允许将Longitude类型的变量分配给类型的变量Latitude

var a = new Latitude(4);
var b: Longitude;
b = a; //error! Cannot convert type 'Latitude' to 'Longitude'

瞧!这就是我们想要的。但是代码很乱,你需要记住转换类型,这真的很不方便,所以实际上不要使用它。但是,正如您所见,它是可能的。

答案 3 :(得分:3)

基于Lodewijk Bogaards answer

interface Casted extends Number {
  DO_NOT_IMPLEMENT
  toManipulate: { castToNumberType:numberType, thenTo: number } 
}

interface LatitudeNumber extends Casted {
  LatitudeNumber
}

interface LongitudeNumber extends Casted {
  LongitudeNumber
}
type numberType = number | Casted
var lat = <LatitudeNumber><numberType>5

function doSomethingStupid(long: LongitudeNumber,lat: LatitudeNumber) {
  var x = <number><numberType>long;
  x += 25;
  return { latitude:lat, longitude:<LongitudeNumber><numberType>x }
}

var a = doSomethingStupid(<LongitudeNumber><numberType>3.067, lat)

doSomethingStupid(a.longitude,a.latitude)

我认为做直接演员会保持名义类型的意图清晰,但遗憾的是需要numberType类型,因为strange design descision铸造数字或数字仍然不会允许添加。转换的javascript非常简单,没有装箱:

var lat = 5;

function doSomethingStupid(long, lat) {
    var x = long;
    x += 25;
    return { latitude: lat, longitude: x };
}
var a = doSomethingStupid(3.067, lat);
doSomethingStupid(a.longitude, a.latitude);

答案 4 :(得分:2)

您可以使用辅助类型在Typescript中近似不透明/名义类型。有关详细信息,请参阅此答案:

// Helper for generating Opaque types.
type Opaque<T, K> = T & { __opaque__: K };

// 2 opaque types created with the helper
type Int = Opaque<number, 'Int'>;
type ID = Opaque<number, 'ID'>;

// works
const x: Int = 1 as Int;
const y: ID = 5 as ID;
const z = x + y;

// doesn't work
const a: Int = 1;
const b: Int = x;

// also works so beware
const f: Int = 1.15 as Int;

这里有一个更详细的答案: https://stackoverflow.com/a/50521248/20489

还有一篇关于不同方法的好文章: https://michalzalecki.com/nominal-typing-in-typescript/

答案 5 :(得分:1)

使用独特的符号 introduced in Typescript 2.7,这实际上可以用两行很好地完成:

declare const latitudeSymbol: unique symbol;
export type Latitude = number & { [latitudeSymbol]: never };

这样,Latitude 就是 number(并且可以像它们一样使用),但普通的 number 不是纬度。

演示

let myLatitude: Latitude;
myLatitude = 12.5 as Latitude; // works
myLatitude = 5; // error
let myOtherLatitude: Latitude = myLatitude // works
let myNumber: number = myLatitude // works
myLatitude = myNumber; // error

const added = myLatitude + myOtherLatitude; // works, result is number

如果忽略第二行,错误信息基本没问题:

Type 'number' is not assignable to type 'Latitude'.
  Type 'number' is not assignable to type '{ [latitudeSymbol]: never; }'.ts(2322)

备注

unique symbol 声明了一个我们需要作为 Latitude 属性的新符号。由于我们不导出符号,因此无法访问它,因此对消费者不可见。

这与biggle's answer中的技巧非常相似,只是它涵盖了评论中的反对意见:

<块引用>

一个问题是人们可能想要访问 __opaque__ – Louis Garczynski

顺便说一句:如果你这样做,你就很好,ReactRedux 正在使用类似的技巧。