Подтвердить что ты не робот

Есть ли способ создать номинальные типы в TypeScript, которые расширяют примитивные типы?

Скажем, у меня есть два типа чисел, которые я отслеживаю как latitude и longitude. Я хотел бы представить эти переменные с помощью базового примитива number, но запретить присваивание переменной longitude a latitude в typescript.

Есть ли способ подкласса примитив number, чтобы typescript обнаружил это назначение как незаконное? В некотором роде для принудительного номинального ввода текста, чтобы этот код не удался?

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

Ответ на "Как продлить примитивный тип в typescript?" кажется, что он поместит меня в правильном направлении, но я не уверен, как чтобы расширить это решение для создания отдельных номинальных подтипов для разных видов.

Нужно ли обертывать примитив? Если да, могу ли я заставить его вести себя довольно легко, как нормальное число, или мне нужно будет ссылаться на под-член? Могу ли я как-то создать подкласс числа typescript для компиляции-времени?

4b9b3361

Ответ 1

Нет способа сделать это.

Предложение, отслеживающее это на сайте GitHub, Единицы измерения.

В следующем выпуске вы сможете использовать type для определения альтернативных имен для примитивов, но у них не будет никакой проверки, связанной с ними:

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

Ответ 2

Вот простой способ добиться этого:

Требования

Вам нужны только две функции, одна из которых преобразует число в числовой тип и одно для обратного процесса. Вот две функции:

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

Как это работает

Что это значит, просто fool Typescript, полагая, что примитивный номер действительно является некоторым расширением интерфейса Number (то, что я называю типом числа), в то время как фактически примитивное число никогда не преобразуется в фактический объект, который реализует число тип. Преобразование не требуется, так как тип числа ведет себя как примитивный тип номера; тип числа просто является числовым примитивом.

Трюк просто набрасывается на any, поэтому Typescript останавливает проверку типов. Поэтому приведенный выше код можно переписать на:

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

doArithmeticAndLog(<any>100);

Как вы можете видеть, вызовы функций даже не нужны, потому что число и его тип номера могут использоваться взаимозаменяемо. Это означает, что во время выполнения требуется полная производительность или потеря памяти. Я по-прежнему настоятельно рекомендую использовать вызовы функций, так как вызов функции стоит почти ничего, и, отбрасывая на any самостоятельно, вы теряете безопасность типа (например, doArithmeticAndLog(<any>'bla') будет компилироваться, но приведет к тому, что NaN войдет в систему на консоли во время выполнения)... Но если вы хотите получить полную производительность, вы можете использовать этот трюк.

Он также может работать для других примитивных, таких как string и boolean.

Счастливая печать!

Ответ 3

На самом деле есть способ достичь того, чего вы хотите достичь, но это немного сложно, имеет некоторые ограничения и может быть совершенно необоснованным для человека, который впервые видит этот код, поэтому относитесь к нему скорее как к любопытству, чем к фактическая реализация;)

Хорошо, так отпустите. Во-первых, нам нужно создать "подкласс" 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(). 2 здесь: во-первых, вам нужно использовать переменные для выполнения этой операции. Во-вторых: результат будет регулярным 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 использует типизацию утки при сравнении типов. Это означает, что два разных типа считаются "совместимыми" (и, следовательно, присваиваются ему, то есть вы можете назначить переменную одного типа значению другого), когда они имеют один и тот же набор свойств. И решение действительно просто: добавьте частного участника. Этот частный член является чисто виртуальным, он нигде не используется и не будет скомпилирован. Но при этом TypeScript считают, что Latitude и Longitude являются совершенно разными типами, и нас больше интересует, не позволит назначать переменную типа Longitude типу Latitude.

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

Voila! Это то, что мы хотели. Но код грязный, и вам нужно помнить о том, чтобы набрасывать типы, что действительно неудобно, поэтому не используйте это на самом деле. Однако, как вы видите, это возможно.

Ответ 4

Основываясь на 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)

Я думаю, что при прямом бросках подразумевается тип номинального типа, тип 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);

Ответ 5

Вы можете аппроксимировать непрозрачные/номинальные типы в 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;

Вот более подробный ответ: fooobar.com/questions/108396/...

Также хорошая статья о разных способах сделать это: https://michalzalecki.com/nominal-typing-in-typescript/

Ответ 6

Пожалуйста, рассмотрите следующий вопрос:

Атомная дискриминация типов (номинальные атомные типы) в TypeScript

С этим пример:

export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                  // Gives compiler error
wi.value = wi.value * 2;              // Gives compiler error
wm.value = wi.value * 2;              // Gives compiler error
const we: MetricWeight = { value: 0 } // Gives compiler error

Ответ 7

Есть еще один способ создания именных типов, который кажется немного более лаконичным. Как обсуждено в Атомной дискриминации типов (номинальные атомные типы) в TypeScript

Основной пример выглядит следующим образом:

export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                  // Gives compiler error
wi.value = wi.value * 2;              // Gives compiler error
wm.value = wi.value * 2;              // Gives compiler error
const we: MetricWeight = { value: 0 } // Gives compiler error