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


假设我有两种类型的数字,我正在跟踪 latitude 和 longitude。我想用基本代表这些变量 number 原始的,但不允许分配 longitude 到了 latitude 打字稿中的变量。

有没有办法分类 number 原语使打字稿检测到此作业是非法的?在某种程度上强迫名义输入,以便此代码失败?

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

答案 “如何在打字稿中扩展原始类型?” 似乎它会让我朝着正确的方向前进,但我不知道如何扩展该解决方案以创建不同的名义子类型  数字。

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


7792
2017-11-07 21:44


起源



答案:


没有办法做到这一点。

在GitHub网站上跟踪此建议的建议是 计量单位

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

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

2
2017-11-07 22:14



我意识到类型别名更容易实现,但是当你可以进行更强大的编译时类型检查时,为什么要引入C / C ++的松弛类型别名而不做依赖于 使用“Apps Hungarian”进行人体模式匹配。无论如何,感谢免费的编译器:-) - Ross Rogers
主要添加类型别名以使处理联合类型更容易。稍微改善基元的体验是其副作用。我希望我们最终可以有一个更好的解决方案,无论是度量单位还是制作基元的品牌子类型的方法。 - Ryan Cavanaugh
对于那些阅读这个。有一种方法:看我的答案。 - Lodewijk Bogaards


答案:


没有办法做到这一点。

在GitHub网站上跟踪此建议的建议是 计量单位

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

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

2
2017-11-07 22:14



我意识到类型别名更容易实现,但是当你可以进行更强大的编译时类型检查时,为什么要引入C / C ++的松弛类型别名而不做依赖于 使用“Apps Hungarian”进行人体模式匹配。无论如何,感谢免费的编译器:-) - Ross Rogers
主要添加类型别名以使处理联合类型更容易。稍微改善基元的体验是其副作用。我希望我们最终可以有一个更好的解决方案,无论是度量单位还是制作基元的品牌子类型的方法。 - Ryan Cavanaugh
对于那些阅读这个。有一种方法:看我的答案。 - Lodewijk Bogaards


这是实现此目的的简单方法:

要求

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

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在运行时登录到控制台)...但是如果你想要完整的性能,你可以使用这个技巧。

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

打字快乐!


9
2018-06-12 16:59





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

好的,让我们走吧。首先,我们需要创建一个“子类” 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 其行为与数字类型相同。你甚至可以加两个 WrappedNumbers - 谢谢你 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

在我看来,这是最棘手的部分。我们现在创建2个类, Latitude 和 Longitude 这将继承自 WrappedNumber (这样他们的行为就像数字一样)

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

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

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

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


2
2017-11-08 23:07



我心中的渴望也是 “类型擦除” 因此运行时成本与使用普通旧数字相同。无论如何,这看起来很有希望。谢谢。 - Ross Rogers
如果事情是没有运行时成本那么可悲的是我很确定这是不可能的,至少对于当前的TypeScript。 - Kuba Jagoda


基于 Lodewijk Bogaards回答

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)

我认为做直接演员会保持名义类型的意图清晰,不幸的是,由于a,需要numberType类型 奇怪的设计决定 转换为数字或数字仍然不允许添加。转换的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);

2
2017-07-02 02:34





您可以使用辅助类型在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/


1
2018-05-25 12:04