Typescript: Предложение: диапазон как числовой тип

Созданный на 30 апр. 2017  ·  106Комментарии  ·  Источник: microsoft/TypeScript

При определении типа можно указать несколько чисел, разделенных | .

type TTerminalColors = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;

Разрешить указывать типы чисел как диапазоны вместо перечисления каждого числа:

type TTerminalColors = 0..15;
type TRgbColorComponent = 0..255;
type TUInt = 0..4294967295;

Возможно, используйте .. для целых чисел и ... для чисел с плавающей запятой.

interface Math {
  random(): 0...1
}
In Discussion Suggestion

Самый полезный комментарий

Эта идея может быть расширена до символов, например, "b".."d" будет "b" | "c" | "d" . Было бы проще указать наборы символов.

Все 106 Комментарий

Эта идея может быть расширена до символов, например, "b".."d" будет "b" | "c" | "d" . Было бы проще указать наборы символов.

Я думаю, что это можно расширить и использовать синтаксис и семантику, например диапазоны Haskell.

| Синтаксис | Desugared |
| -------------------------- | ---------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------------------------------ |
| type U = (e1..e3) | type U = \| e1 \| e1+1 \| e1+2 \| ...e3 \|
Объединение равно never если e1 > e3 |
| type U2 = (e1, e2..e3) | type U2 = \| e1 \| e1+i \| e1+2i \| ...e3 \| ,
где приращение i равно e2-e1 .

Если приращение положительное или нулевое, объединение завершается, когда следующий элемент будет больше, чем e3 ;
союз равен never если e1 > e3 .

Если приращение отрицательное, объединение завершается, когда следующий элемент окажется меньше e3 ;
союз равен never если e1 < e3 . |

@panuhorsmalahti Что делать, если вы укажете "bb".."dd" ?

@streamich

Может быть, использовать .. для целых чисел и ... для чисел с плавающей запятой.

Мне очень нравится идея создания таких целочисленных типов, но я не понимаю, как могут работать значения с плавающей запятой.

@aluanhaddad Скажи вероятность:

type TProbability = 0.0...1.0;

@streamich, так что у этого типа теоретически бесконечное количество возможных жителей?

@aluanhaddad на самом деле было бы далеко не бесконечно в IEEE с плавающей запятой. По моим подсчетам, там будет 1 065 353 217 жителей.

0.0...1.0 ? JS использует IEEE double, то есть 53 бита динамического диапазона. Если бы это было поддержано, диапазоны должны были бы быть типом первого класса, отказ от объединения в объединение было бы непрактичным.

@jcready действительно, но, как указывает @fatcerberus , реализация его как типа объединения была бы чрезмерно обширной.

Я хотел окольными путями ввести в язык некоторое понятие дискретных и непрерывных типов.

реализация его как профсоюзного типа была бы чрезмерно обширной.

@aluanhaddad Да, но даже указание целого числа без знака в виде объединения было бы очень дорогостоящим:

type TUInt = 0..4294967295;

Для этого действительно нужны убедительные примеры использования, потому что реализация союзов сегодня совершенно не подходит для реализации таких больших союзов. Что-то, что произойдет, если вы напишете что-то вроде этого

type UInt = 0..4294967295;
var x: UInt = ......;
if (x !== 4) {
  x;
}

будет экземпляром типа объединения 0 | 1 | 2 | 3 | 5 | 6 | 7 | ... .

Возможно, это могло работать только с числовыми литералами. Любые нелитеральные числовые значения должны быть явно уточнены с помощью сравнений «больше / меньше, чем», прежде чем они будут считаться заселяющими диапазон. Целочисленные диапазоны также потребуют дополнительной проверки Number.isInteger() . Это должно устранить необходимость создания фактических типов объединения.

@RyanCavanaugh Типы вычитания? 🌞

Отрицательные типы, отрицание типа.

Что угодно, кроме строки:

type NotAString = !string;

Любое число кроме нуля:

type NonZeroNumber = number & !0;

Типы вычитания

Мой вариант использования: я хотел бы ввести параметр как 0 или положительное число (это индекс массива).

@RoyTinker Я определенно думаю, что это было бы круто, но я не знаю, помогает ли этот вариант использования аргументу.
Массив - это просто объект, а возрастающие индексы - это просто соглашение.

let a = [];
for (let i = 0; i > -10; i -= 1) {
  a[i] = Math.random() * 10;
}

так что в конечном итоге вам все равно придется выполнить ту же проверку

function withItem<T>(items: T[], index: number, f: (x: T) => void) {
  if (items[index]) {
    f(items[index]);
  }
}

Это было бы весьма полезно для определения таких типов, как секунда, минута, час, день, месяц и т. Д.

@Frikki, эти единицы находятся в достаточно ограниченном интервале, поэтому их практически и непозволительно сложно записать вручную.

type Hour =
   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
   | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23;

@aluanhaddad Но нет беззнакового int:

type UInt = 0..4294967295;

а как насчет такого типа:

type Factorial<N extends number> = N > 2 ? Factorial<N - 1> * N : N;
type F1 = Factorial<1>; // 1
type F2 = Factorial<2>; // 1 | 2
type F3 = Factorial<3>; // 1 | 2 | 6
type FN = Factorial<number>; // 1 | 2 | 6 | ... 

Использование оператора * из комментария @ aleksey-bykov:

type char = 0..255;
type word = char ** 2;
type int = word ** 2;
type bigint = int ** 2;

@streamich Удвоение количества битов не соответствует умножению на два, это больше похоже на возведение в степень с 2 в качестве показателя степени. Однако это все еще неверно, так как вы не должны повышать верхнюю границу, но учитываются кодируемые числа. В общем, это не самая лучшая стратегия определения.

@streamich , некоторые комментарии:

  • Использование терминов char , word и т. Д. Может сбивать с толку, поскольку люди из других языков могут не осознавать разницу между статическим определением и поведением во время выполнения.
  • Предлагаемый вами синтаксис не учитывает нижнюю границу - что, если она не равна нулю?
  • Я бы с осторожностью относился к использованию оператора возведения в степень для использования в контексте окружения / типа, поскольку он уже был добавлен в ES2016.

давайте просто завершим систему типов по Тьюрингу и насладимся проблемой остановки, когда мы нажмем Ctrl + Shift + B

@ aleksey-bykov наверняка ты помнишь этот прекрасный выпуск 😀

@aluanhaddad

эти единицы [единицы времени] находятся в достаточно ограниченном интервале, поэтому их практически и недопустимо сложно записать от руки.

https://github.com/Microsoft/TypeScript/issues/15480#issuecomment -349270853

Теперь сделайте это за миллисекунды: wink:

Эта проблема мертва?

@Palid помечен тегом [Needs Proposal] поэтому я сомневаюсь в этом.

Каким бы увлекательным ни было обсуждение, большинству из нас не удалось предоставить убедительные примеры использования в реальном мире.

См. Https://github.com/Microsoft/TypeScript/issues/15480#issuecomment -324152700

__СЛУЧАИ ПРИМЕНЕНИЯ:__

  1. Вы можете определить точные типы int, чтобы подмножество TypeScript можно было скомпилировать для WebAssembly или какой-либо другой цели.

  2. Другой вариант использования - UInt8Array , Int8Array , Uint16Array и т. Д., Когда вы читаете или записываете данные из них, TypeScript может проверять наличие ошибок.

const ab = new ArrayBuffer(1e3);
const uint8 = new UInt8Array(ab);

uint8[0] = 0xFFFFFFFF; // TSError: Number too big!
  1. Я упомянул несколько вариантов использования в своем OP.

  2. Если вы реализуете это, сообщество TypeScript предложит миллионы вариантов использования.

У меня есть действительно забавный вариант использования, который на самом деле может быть ближе ко всему, что предлагается здесь.
API принимает целое число в некотором диапазоне (в моем случае 5-30), и для этого нам нужно реализовать SDK.
Вручную писать внизу утомительно (хотя это можно отчасти автоматизировать) для 25 значений, но как насчет сотен или тысяч?
type X = 5 | 6 | 7 | 8 | 9 | 10 ... | 30

@Palid Это лучший и самый простой случай, который я видел для этой функции.

Если бы диапазон вроде 5..30 был определен как синтаксический сахар для 5 | 6 | 7 ... | 30 (или функций, идентичных ему), я уверен, что это будет легкая победа. Здесь это будет обозначать дискретный диапазон целых чисел.

Возможно, непрерывный диапазон (в отличие от дискретного) можно обозначить с помощью числа с точкой - 5.0..30.0 .

На самом деле я подумывал о том, чтобы взглянуть на Typescript, чтобы реализовать защиту ввода для
https://www.npmjs.com/package/memory-efficient-object

Наличие набора текста по диапазону упростит выявление потенциального переполнения памяти во время проверки типа.

Похоже, что нам действительно не нужно расширение объединения, но больше похоже на определение включающего / исключающего диапазона, которое просто сравнивает диапазон

type TTerminalColors = int & [0,15];
// int && v >= 0 && v <= 15

Это было бы очень полезно при управлении двигателями (например, с помощью Johnny-Five), где скорость варьируется от 0 до 255.

Другой вариант использования: я реализую программу рисования на основе Canvas с использованием TypeScript и хотел бы иметь проверку типа на непрозрачность (которая должна быть числом от 0,0 до 1,0).

Просто подумайте ... чтобы реализовать это должным образом, вам нужно действительно изо всех сил:

  • поддержка функций защиты типа времени выполнения
  • сужение типов для таких условий, как x <= 10
  • поддержка диапазонов string | number (поскольку x == 5 - это true для x === "5" ) в дополнение к диапазонам только номеров
  • возможно даже поддержка нового типа int (который будет подтипом number ) и поддержка диапазонов int -only и string | int . Также введите сужение для таких выражений, как x|0

Отличная идея, но это было бы слишком сложно!

Может быть, нам не нужна защита типа времени выполнения. Вместо этого мы могли бы сделать защиту времени компиляции полностью

Вместо этого потребуется подробное предсказание ветвления.

Предполагать

type TTerminalColors = int & [0,15];

function A(color: TTerminalColors):void
{

}

A(15); // OK
var x = 15;
A(x); // OK

function B(value: int) : void
{
    A(value); // ERROR!!!
    if(value >= 0 && value <= 15)
        A(value); // OK, because we check that it is in the range of TTerminalColors
}

function C(value: int) : void
{
    if(value < 0)
        value = 0;
    if(value > 15)
        value = 15;

    A(value); // OK, because is clamped. But maybe too hard to implemented
}

function ClampInt(value: int): TTerminalColors
{
    if(value >= 0 && value <= 15)
        return value; // Same as B(int)

    if(value > 15)
        return 15;

    return 0;
}

Мой вариант использования:

export const ajax: (config: AjaxOptions) => void;

type Callback = Success | Error;
type Success = (data: any, statusText: string, xhr: XMLHttpRequest) => void;
type Error = (xhr: XMLHttpRequest, statusText: string) => void;

interface AjaxOptions {
  // ...
  statusCode: { [code: number]: Callback | Callback[] },
  // ...
}

Было бы неплохо иметь возможность ограничить ключи в параметре statusCode , чтобы во время компиляции можно было определить, соответствует ли код состояния успешному или коду ошибки:

interface AjaxOptions {
  // ...
  statusCode: {
    200..300: Success,
    400..600: Error
  },
  // ...
}

ИМО это должно быть ограничено числами с плавающей запятой и целыми числами и не должно быть реализовано как объединение, а скорее как новый тип диапазона. Тогда проверка типов будет такой же простой, как:

if (val >= range.start && val < range.end) {
  return match;
} else {
  return no_match;
}

Возможно, мы могли бы взять листок из книги Руби и использовать .. для включающих диапазонов ( [start, stop] ) и ... для не включающих диапазонов ( [start, stop) ).

Другой вариант использования - ввести проверку записей в базе данных:

Другой вариант использования - проверка типов значений широты и долготы.

Это может быть покрыто общим механизмом проверки # 8665 (не уверен, почему он был закрыт как дубликат):

type TTerminalColors (n: number) => Math.floor(n) == n && n >= 0 && n <= 15;
type TRgbColorComponent (n: number) => Math.floor(n) == n && n >= 0 && n <= 255;
type TUInt (n: number) => n >= 0 && n <= 0..4294967295;

Или взято с # 4639 и предполагая, что целочисленный тип без знака определен как uint :

type TTerminalColors (n: uint) => n <= 15;
type TRgbColorComponent (n: uint) => n <= 255;

Мой вариант использования - глобальные координаты. Я хочу ввести широту и долготу, чтобы они попадали только в определенные диапазоны (от -90 до 90 и от -180 до 180).

Изменить: широта и долгота имеют отрицательный диапазон

Мой вариант использования - это реализация массивов фиксированного размера, где размер является параметром.

Например, я хочу определить тип для массивов строк длиной 2.

let d: FixedSizeArray<2, string>;
d = [ 'a', 'b' ]; // ok
d = [ 'a' ]; // type error
d = [ 'a', 'b', 'c' ]; // type error
d[0] = 'a1'; // ok
d[1] = 'b1'; // ok
d[2] = 'c1' // type error

В текущей версии TS можно определить что-то очень близкое к "спецификации" выше. Основная проблема - доступ к члену: например, d[1] = 'b1' возвращает ошибку типа, даже если она верна. Чтобы избежать ошибки, список всех юридических индексов должен быть составлен вручную в определении FixedSizeArray , что утомительно.

Если бы у нас был оператор range аналогичный оператору keyof , следующее определение типа должно было бы решить проблему.

type FixedSizeArray<U extends number, T> = {
    [k in range(U)]: T;
} & { length: U };

Где range(N) - это ярлык для range(0,N) .

Для числовых литералов (натуральных) M, N с M <N,

type r = range(M, N); 

эквивалентно

type r = M | M+1 | ... | N-1

Что могло бы быть более общим, так это если бы у нас была возможность определять лямбда-выражения предикатов и использовать их как типы. Пример:

predicate mypredicate = (x) => x > 1 && x < 10 
let x: mypredicate = 11 // not ok
let x: mypredicate = 5 // ok

Профессионал будет с низкой сложностью синтаксиса, поскольку лямбды уже доступны, все, что нам нужно, это возможность использовать их в качестве типов, проверка типов в любом случае зависит от машинописного текста (имея в виду философию "надмножества Javascript")
Минус в том, что сложность предиката будет определять производительность инструментария для обеспечения обратной связи.

Обеспечение принадлежности числа / символа к арифметической прогрессии может быть хорошим распространенным вариантом использования:

// a is the starting element d is the difference between two elements and L is the last element
const belongsToAP = (a, d, L) => {
  return (x) => {
    if(x < a || x > L) return false
    let n = ((x-a)/d) + 1
    if(Number.isInteger(n)) return true
    return false
  }
}

это позволит нам выполнять такие проверки типов, как:
предикат ownToMyAP = ownToAP (1,1, 10)

let x : belongsToMyAP = 5 // ok
let y : belongsToMyAP = 7.2 // not ok

Это также может быть расширено до персонажей.

@Kasahs Нечто подобное уже было предложено в # 8665.

Вкладываю свою долю в сценарий использования «отражение API». Конечная точка REST, для которой я пишу функцию-оболочку, принимает в качестве аргумента целое число в диапазоне 1..1000. Он генерирует ошибку, если число не удовлетворяет этому ограничению.

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

Поскольку компилятор TypeScript написан на TypeScript, можно использовать свойства числовых операторов для изменения типов диапазонов.

// Syntax: x..y for an inclusive integer range.

let x: 0..10 = randomNumber(0, 10);
let y = x + 2; // Can deduce that y: 2..12.

Это нормально при присвоении новой переменной, но как насчет мутации?

let x: 0..10 = randomNumber(0, 10);
x += 2; // Error: 2..12 is not assignable to type 0..10 (upper bound is out of range).

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

let x = randomNumber(0, 10) as number; // If randomNumber doesn't return a type assigable to number
// this will be an error, but it would still be annoying to have to sprinkle "as number"
// expressions everywhere.

А если проигнорировать это, мы получим следующую несостоятельность:

function logNumber0To10 (n: 0..10): void {
    console.log(n);
}

let x: 0..10 = randomNumber(0, 10);
x += 2; // Because we're ignoring mutations, x: 0..10, but the runtime value could be 11 or 12,
// which are outside the specified range...
logNumber0To10(x); // ...which means we lose type safety on this call.

Исправлением для этого была бы возможность изменить тип переменной после ее объявления, поэтому в примере 2 просто изменится тип x на 2..12; но мой первый инстинкт состоит в том, что это приведет к слишком большим накладным расходам в компиляторе и запутает пользователей.

А как насчет пользовательских функций и дженериков?

// How to define this return type, seeing as we can't do math in types?
function increment<L extends number, H extends number> (x: L..H): (L + 1)..(H + 1);

Есть мысли о том, как с этим справиться?

@JakeTunaley

а что насчет мутации?

Почему мутация должна отличаться от любого другого назначения?

Вот моя скромная попытка сделать предложение. Это предложение также требует добавления других типов.

  • Infinity тип

    • Имеет только значение Infinity

  • -Infinity тип

    • Имеет только значение -Infinity

  • NaN тип

    • Имеет только значение NaN

  • double тип

    • Все значения number в диапазоне [-Number.MAX_VALUE, Number.MAX_VALUE] или [-1.7976931348623157e+308, 1.7976931348623157e+308]

  • number - это всего лишь Infinity|-Infinity|NaN|double
  • int тип

    • Подтип double

    • Все number значения x в диапазоне [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] или [-9007199254740991, 9007199254740991] и Math.floor(x) === x

    • Итак, 3 и 3.0 будут значениями типа int

  • Тип "Конечный литерал"

    • Примеры: 1 , 3.141 , 45

    • Могут быть подтипы double или int

  • Тип "GtEq" обозначен как (>= x)

    • Где x - конечный литерал, Infinity или -Infinity

  • Тип "LtEq" обозначается (<= x)

    • Где x - конечный литерал, Infinity или -Infinity

  • Тип "Gt" обозначается (> x)

    • Где x - конечный литерал, Infinity или -Infinity

  • Тип "Lt" обозначается (< x)

    • Где x - конечный литерал, Infinity или -Infinity


GtEq type; (>= x)

  • (>= Infinity) = Infinity
  • (>= -Infinity) = -Infinity|double|Infinity
  • Infinity - это подтип (>= [finite-literal])
  • (>= [finite-literal]) - это подтип double|Infinity
  • (>= NaN) = never
  • (>= int) = (>= -9007199254740991)
  • (>= double) = (>= -1.7976931348623157e+308)
  • (>= number) = number

Тип GT; (> x)

  • (> Infinity) = never
  • (> -Infinity) = double|Infinity
  • Infinity - это подтип (> [finite-literal])
  • (> [finite-literal]) - это подтип double|Infinity
  • (> NaN) = never
  • (> int) = (> -9007199254740991)
  • (> double) = (> -1.7976931348623157e+308)
  • (> number) = number

Тип LtEq; (<= x)

  • (<= Infinity) = -Infinity|double|Infinity
  • (<= -Infinity) = -Infinity
  • -Infinity - это подтип (<= [finite-literal])
  • (<= [finite-literal]) - это подтип -Infinity|double
  • (<= NaN) = never
  • (<= int) = (<= 9007199254740991)
  • (<= double) = (<= 1.7976931348623157e+308)
  • (<= number) = number

Тип Lt; (< x)

  • (< Infinity) = -Infinity|double
  • (< -Infinity) = never
  • -Infinity - это подтип (< [finite-literal])
  • (< [finite-literal]) - это подтип -Infinity|double
  • (< NaN) = never
  • (< int) = (< 9007199254740991)
  • (< double) = (< 1.7976931348623157e+308)
  • (< number) = number

Типы диапазонов

Обратите внимание: хотя мы можем писать такие вещи, как (>= Infinity) , (> number) и т. Д.,
результирующий тип не является диапазоном; они просто псевдонимы для других типов.

Тип диапазона - это один из следующих:

  • (>= [finite-literal])
  • (> [finite-literal])
  • (<= [finite-literal])
  • (< [finite-literal])

Мы разрешаем использовать синтаксис типа (> number) и аналогичный для использования в дженериках.


Типы Union GtEq / Gt

При объединении двух типов GtEq / Gt результатом является тип с "дополнительными" значениями,

  • (>= [finite-literal-A]) | (>= [finite-literal-B]) = ...

    • Если [finite-literal-A] >= [finite-literal-B] , то результат будет (>= [finite-literal-B])

    • В противном случае результат будет (>= [finite-literal-A])

    • например, (>= 3) | (>= 5.5) = (>= 3) потому что (>= 3) является супертипом (>= 5.5)

  • (>= [finite-literal-A]) | (> [finite-literal-B]) = ...

    • Если [finite-literal-A] == [finite-literal-B] , то результат будет (>= [finite-literal-A])

    • Если [finite-literal-A] > [finite-literal-B] , то результат будет (> [finite-literal-B])

    • В противном случае результат будет (>= [finite-literal-A])

  • (> [finite-literal-A]) | (> [finite-literal-B]) = ...

    • Если [finite-literal-A] >= [finite-literal-B] , то результат будет (> [finite-literal-B])

    • В противном случае результат будет (> [finite-literal-A])

    • например, (> 3) | (> 5.5) = (> 3) потому что (> 3) является супертипом (> 5.5)

Также,

  • (>= A|B) = (>= A) | (>= B)
  • (> A|B) = (> A) | (> B)

    • например, (> 4|3) = (> 4) | (> 3) = (> 3)

    • например, (> number|3) = (> number) | (> 3) = number | (> 3) = number

Типы Union LtEq / Lt

При объединении двух типов LtEq / Lt результатом является тип с "дополнительными" значениями,

  • (<= [finite-literal-A]) | (<= [finite-literal-B]) = ...

    • Если [finite-literal-A] <= [finite-literal-B] , то результат будет (<= [finite-literal-B])

    • В противном случае результат будет (<= [finite-literal-A])

    • например, (<= 3) | (<= 5.5) = (<= 5.5) потому что (<= 5.5) является супертипом (<= 3)

  • (<= [finite-literal-A]) | (< [finite-literal-B]) = ...

    • Если [finite-literal-A] == [finite-literal-B] , то результат будет (<= [finite-literal-A])

    • Если [finite-literal-A] < [finite-literal-B] , то результат будет (< [finite-literal-B])

    • В противном случае результат будет (<= [finite-literal-A])

  • (< [finite-literal-A]) | (< [finite-literal-B]) = ...

    • Если [finite-literal-A] <= [finite-literal-B] , то результат будет (< [finite-literal-B])

    • В противном случае результат будет (< [finite-literal-A])

    • например, (< 3) | (< 5.5) = (< 5.5) потому что (< 5.5) является супертипом (< 3)

Также,

  • (<= A|B) = (<= A) | (<= B)
  • (< A|B) = (< A) | (< B)

    • например, (< 4|3) = (< 4) | (< 3) = (< 4)

    • например, (< number|3) = (< number) | (< 3) = number | (< 3) = number


Пересечение типов GtEq / Gt

При пересечении двух типов GtEq / Gt результатом является тип с "меньшим количеством" значений,

  • (>= [finite-literal-A]) & (>= [finite-literal-B]) = ...

    • Если [finite-literal-A] >= [finite-literal-B] , то результат будет (>= [finite-literal-A])

    • В противном случае результат будет (>= [finite-literal-B])

    • например, (>= 3) & (>= 5.5) = (>= 5.5) потому что (>= 5.5) является подтипом (>= 3)

  • (>= [finite-literal-A]) & (> [finite-literal-B]) = ...

    • Если [finite-literal-A] == [finite-literal-B] , то результат будет (> [finite-literal-B])

    • Если [finite-literal-A] > [finite-literal-B] , то результат будет (>= [finite-literal-A])

    • В противном случае результат будет (> [finite-literal-B])

  • (> [finite-literal-A]) & (> [finite-literal-B]) = ...

    • Если [finite-literal-A] >= [finite-literal-B] , то результат будет (> [finite-literal-A])

    • В противном случае результат будет (> [finite-literal-B])

    • например, (> 3) & (> 5.5) = (> 5.5) потому что (> 5.5) является подтипом (> 3)

Пересечение типов LtEq / Lt

При пересечении двух типов LtEq / Lt результатом является тип с "меньшим количеством" значений,

  • (<= [finite-literal-A]) & (<= [finite-literal-B]) = ...

    • Если [finite-literal-A] <= [finite-literal-B] , то результат будет (<= [finite-literal-A])

    • В противном случае результат будет (<= [finite-literal-B])

    • например, (<= 3) & (<= 5.5) = (<= 3) потому что (<= 3) является подтипом (<= 5.5)

  • (<= [finite-literal-A]) & (< [finite-literal-B]) = ...

    • Если [finite-literal-A] == [finite-literal-B] , то результат будет (< [finite-literal-B])

    • Если [finite-literal-A] < [finite-literal-B] , то результат будет (<= [finite-literal-A])

    • В противном случае результат будет (< [finite-literal-B])

  • (< [finite-literal-A]) & (< [finite-literal-B]) = ...

    • Если [finite-literal-A] <= [finite-literal-B] , то результат будет (< [finite-literal-A])

    • В противном случае результат будет (< [finite-literal-B])

    • например, (< 3) & (< 5.5) = (< 3) потому что (< 3) является подтипом (< 5.5)


Случаи применения

  • Чтобы статически гарантировать, что целое число может соответствовать типу данных MySQL UNSIGNED INT ,

    //TODO Propose numeric and range sum/subtraction/multiplication/division/mod/exponentiation types?
    function insertToDb (x : int & (>= 0) & (<= 4294967295)) {
      //Insert to database
    }
    
  • Чтобы статически гарантировать, что строка имеет заданную длину,

    function foo (s : string & { length : int & (>= 1) & (<= 255) }) {
      //Do something with this non-empty string that has up to 255 characters
    }
    
  • Чтобы статически гарантировать, что объект, подобный массиву, имеет соответствующее значение length ,

    function foo (arr : { length : int & (>= 0) }) {
      //Do something with the array-like object
    }
    
  • Чтобы статически гарантировать, что нам даны только конечные числа,

    function foo (x : double) {
      //`x` is NOT NaN|Infinity|-Infinity
    }
    
  • Статически гарантировать, что индексы массива существуют?

    function foo (arr : { [index : int & (>= 0) & (< 10)] : string }) {
      console.log(arr[0]); //OK
      console.log(arr[1]); //OK
      console.log(arr[2]); //OK
      console.log(arr[9]); //OK
      console.log(arr[10]); //Error
    }
    

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

[Редактировать]
Вы можете переименовать double и назвать его float но я просто подумал, что double более точно представляет, что это число с плавающей запятой двойной точности.

[Редактировать]

Изменены некоторые типы на never .

Можно ли, чтобы компилятор выполнял анализ потока?

Предположим, есть эта функция

function DoSomething(x : int & (>= 0) & (< 10)){
   // DoSomething
}

function WillError(x : int){
    DoSomething(x); // error; x is not >= 0 & < 10
}

function WillNotError(x : int){
    if(x >= 0 && x < 10)
        DoSomething(x); // not error by flow analysis
}

еще один вариант использования: у меня есть числовой ввод для функции, представляющий процент. Я хочу ограничить значения между 0 и 1.

Я просто запустил [...Array(256)].map((_,i) => i).join("|") чтобы сделать свое самое уродливое определение типа

Возможны целые неотрицательные и малые числа:

type ArrayT<T> = T extends (infer P)[] ? P : never;
type A = ArrayT<Range<5, 10>>;//5|6|7|8|9|10

Диапазон: https : //github.com/kgtkr/typepark/blob/master/src/list.ts

Под "маленькими числами" вы подразумеваете без использования стадии 3 BigInt ?

@Mouvedia Число, которое соответствует пределу рекурсии компилятора

Может быть, использовать .. для целых чисел и ... для чисел с плавающей запятой.

Я бы сказал, что .. должно означать включающий диапазон, а ... исключительный. Так же, как, например, в Ruby. См. Http://rubylearning.com/satishtalim/ruby_ranges.html

Я не поклонник использования одной точки, чтобы различать инклюзивные и эксклюзивные диапазоны.

Могу я прыгнуть? Я думаю, что эта функция, рассматриваемая как единственное усовершенствование для спецификации типа, вряд ли лучший способ здесь.

type range = 1:2:Infinity // 1, 3, 5… Infinity

Так вы определяете числовой диапазон на многих платформах 4gl, особенно на матрично-ориентированных.

Вы делаете это таким образом, потому что равномерный диапазон традиционно является одним из лучших подходов к моделированию.

Итак, последовательный диапазон таков:

type decmials = 1:10 // like 1 .. 10 with all decimals in between

А если только целые числа:

type integers = 1:1:10 // 1, 2, 3, 4, 5, 6, 7, 8, 10

// OR

type integers = number.integers<1:10>

Если мы хотим поэкспериментировать с синтаксисом:

type something = 1::10 // whatever use cases need today

Если это не подходит для токенизатора, на мой взгляд, это неправильный приоритет блокировки, на котором стоит сосредоточиваться. Я указываю на это в надежде, что одно решение не ограничит нас в переходе к следующему.

edit : Что я не могу учесть, так это аспект инклюзивности - и здесь мы должны задаться вопросом, делаем ли мы достаточно должной осмотрительности, чтобы понять, почему это не было проблемой, когда люди решали так много проблем, полагаясь на единые диапазоны, которые неявно включают каждое число за исключением случаев, когда приращение точно не совпадает с концом диапазона.

Интересно, что вы упомянули возможность определять размеры «шага» в типе диапазона.

Помимо диапазонов «с плавающей запятой» (без фиксированного размера шага) и «целых» (размер шага 1, начинается с целого числа), я никогда не встречал реальных случаев использования диапазонов с другими размерами шага.

Так что интересно услышать об этом где-то еще. Теперь мне нужно узнать о 4gl, потому что я никогда о нем раньше не слышал.


Возможность определения полуоткрытого интервала полезна для объектов, подобных массиву. Что-то вроде,

interface SaferArray<T, LengthT extends integer & (>= 0)> {
    length : LengthT;
    [index in integer & (>= 0) & (< LengthT)] : T
}

Если бы у нас были только инклюзивные диапазоны, нам понадобилось бы (<= LengthT - 1) но это менее элегантно

Могу я прыгнуть? Я думаю, что эта функция, рассматриваемая как единственное усовершенствование для спецификации типа, вряд ли лучший способ здесь.

type range = 1:2:Infinity // 1, 3, 5… Infinity

Так вы определяете числовой диапазон на многих платформах 4gl, особенно на матрично-ориентированных.

Вы делаете это таким образом, потому что равномерный диапазон традиционно является одним из лучших подходов к моделированию.

Итак, последовательный диапазон таков:

type decmials = 1:10 // like 1 .. 10 with all decimals in between

А если только целые числа:

type integers = 1:1:10 // 1, 2, 3, 4, 5, 6, 7, 8, 10

// OR

type integers = number.integers<1:10>

Если мы хотим поэкспериментировать с синтаксисом:

type something = 1::10 // whatever use cases need today

Если это не подходит для токенизатора, на мой взгляд, это неправильный приоритет блокировки, на котором стоит сосредоточиваться. Я указываю на это в надежде, что одно решение не ограничит нас в переходе к следующему.

edit : Что я не могу учесть, так это аспект инклюзивности - и здесь мы должны задаться вопросом, делаем ли мы достаточно должной осмотрительности, чтобы понять, почему это не было проблемой, когда люди решали так много проблем, полагаясь на единые диапазоны, которые неявно включают каждое число за исключением случаев, когда приращение точно не совпадает с концом диапазона.

Хм, похоже, как это делается в Haskell. Думаю, это хорошо. Генераторы также допускают ленивую оценку.

Не похоже, что у вас не может быть ленивой оценки с любым другим синтаксисом = x

Интересно, что вы упомянули возможность определять размеры «шага» в типе диапазона.

Я думаю о диапазоне как о начале, конце и приращении. Итак, если приращение округляется точно до конца, то оно включает его. Индексы и длины массива, как функции диапазона, могут быть массивом 0: 1: 9 с 10 шагами индекса (длина). Таким образом, диапазон здесь может удобно составлять integer & 0:9 для системы типов, которая может легче вывести из объединения этих двух выражений.

4GL - это действительно общий лейбл, для меня это был в основном MatLab.

Я хотел сказать, что наличие только инклюзивных диапазонов затруднит его использование в универсальных шаблонах (если вы не реализуете математические операции на уровне типа hack-y).

Потому что вместо того, чтобы иметь в качестве параметра типа только «длину», теперь вам нужны длина и максимальный индекс. И максимальный индекс должен быть равен length-1.

И, опять же, вы не можете проверить, что это так, если не реализуете эти математические операции на уровне хакерского типа.

@AnyhowStep Я думал о том, как лучше всего

Если мы отложим в сторону включение-или-не как конкретно применимую к любой проблеме n-1 (например, этот сценарий индекса / подсчета), предполагая, что это было каким-то образом независимо решено (просто шутите над мыслью), существуют ли другие сценарии, для которых числовые диапазоны будут по-прежнему требуется менее декларативный и / или нетрадиционный синтаксис для правильного описания?

Я понимаю, что это два отдельных аспекта, вам нужны числовые диапазоны, которые условно согласованы с ожиданиями этого домена, и вам также нужны предикаты для типов индекса / подсчета ... и т. Д.

Только положительные числа.

declare let x : (> 0);
x = 0.1; //OK
x = 0.0001; //OK
x = 0.00000001; //OK
x = 0; //Error
x = 1; //OK
x = -1; //Error

В диапазонах только для включения

declare let x : epsilon:epsilon:Infinity; //Where epsilon is some super-small non-zero, positive number

Положительные числа, за исключением Infinity ,

declare let x : (> 0) & (< Infinity);

В диапазонах только для включения

const MAX_FLOAT : 1.7976931348623157e+308 = 1.7976931348623157e+308;
declare let x : epsilon:epsilon:MAX_FLOAT;

Кроме того, не имеет отношения к текущему обсуждению, но вот еще одна причина, по которой мне действительно нужны числовые типы диапазонов.

Большая часть моей повседневной работы - это просто собирать вместе кучу разных программных систем. И многие из этих систем предъявляют разные требования к данным, имеющим одно и то же значение.

например, (systemA, значение от 1 до 255), (systemB, значение от 3 до 73) и т. д.
например (systemC. длина строки 7-88), (systemD, длина строки 9-99), (systemE, длина строки 2-101) и т. д.

Прямо сейчас мне нужно тщательно задокументировать все эти отдельные требования и убедиться, что данные из одной системы можно правильно сопоставить с другой системой. Если это не отображается, мне нужно найти обходные пути.

Я всего лишь человек. Я делаю ошибки. Я не понимаю, что иногда данные нельзя отобразить. Проверка диапазона терпит неудачу.

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


Например, у меня была ситуация, когда API, который я использовал, имел ограничение на длину строки 10 КБ для всех строковых значений. Что ж, у меня не было возможности указать TypeScript проверять, что все строки, идущие в API, имеют длину <= 10 тыс. Строк.

У меня была ошибка времени выполнения вместо красивой ошибки компиляции, куда мог пойти TS,

`string` is not assignable to `string & { length : (<= 10000) }`

@AnyhowStep Я надеюсь, вы понимаете, что мое намерение состоит в том, чтобы просто убедиться, что если что-то под названием «Диапазон как числовой тип» просто согласуется с обычными ожиданиями более обычного пользователя (например, кто-то, переходящий в TS, задается вопросом, почему диапазон подчеркивает инклюзивность по сравнению с интервалом)

Я честно считаю, что варианты использования всегда должны определять функции, и думаю, что проблемы, связанные с индексом и длиной, - это те, которые многие из нас иногда действительно упускают. Поэтому я просто хочу решить эту проблему под правильным названием - это числовой тип или индексированный тип? Я не знаю точного ответа, но не решаюсь думать, что решение этого вопроса как числового типа непреднамеренно не создаст гораздо больше проблем для пользователей числового типа, которые не разделяют одинаковое понимание этих аспектов.

Итак, где еще решение такой проблемы могло бы иметь смысл для всех, - это все, что меня интересует на данный момент, мысли?

Я имею дело с API, который передает байтовые массивы, поэтому я хотел бы определить байтовый тип:

type byte = 0x00..0xFF
type bytes = byte[]

Это также будет полезно при работе с Uint8Array .

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

TL; DR,
Типы диапазонов теперь могут работать

interface CompileError<_ErrorMessageT> {
    readonly __compileError : never;
}
///////////////////////////////////////////////
type PopFront<TupleT extends any[]> = (
    ((...tuple : TupleT) => void) extends ((head : any, ...tail : infer TailT) => void) ?
    TailT :
    never
);
type PushFront<TailT extends any[], FrontT> = (
    ((front : FrontT, ...tail : TailT) => void) extends ((...tuple : infer TupleT) => void) ?
    TupleT :
    never
);
type LeftPadImpl<TupleT extends any[], ElementT extends any, LengthT extends number> = {
    0 : TupleT,
    1 : LeftPad<PushFront<TupleT, ElementT>, ElementT, LengthT>
}[
    TupleT["length"] extends LengthT ?
    0 :
    1
];
type LeftPad<TupleT extends any[], ElementT extends any, LengthT extends number> = (
    LeftPadImpl<TupleT, ElementT, LengthT> extends infer X ?
    (
        X extends any[] ?
        X :
        never
    ) :
    never
);
type LongerTuple<A extends any[], B extends any[]> = (
    keyof A extends keyof B ?
    B :
    A
);

///////////////////////////////////////////////////////
type Digit = 0|1|2|3|4|5|6|7|8|9;
/**
 * A non-empty tuple of digits
 */
type NaturalNumber = Digit[];

/**
 * 6 - 1 = 5
 */
type SubOne<D extends Digit> = {
    0 : never,
    1 : 0,
    2 : 1,
    3 : 2,
    4 : 3,
    5 : 4,
    6 : 5,
    7 : 6,
    8 : 7,
    9 : 8,
}[D];

type LtDigit<A extends Digit, B extends Digit> = {
    0 : (
        B extends 0 ?
        false :
        true
    ),
    1 : false,
    2 : LtDigit<SubOne<A>, SubOne<B>>
}[
    A extends 0 ?
    0 :
    B extends 0 ?
    1 :
    2
];


//false
type ltDigit_0 = LtDigit<3, 3>;
//true
type ltDigit_1 = LtDigit<3, 4>;
//false
type ltDigit_2 = LtDigit<5, 2>;


/**
 * + Assumes `A` and `B` have the same length.
 * + Assumes `A` and `B` **ARE NOT** reversed.
 *   So, `A[0]` is actually the **FIRST** digit of the number.
 */
type LtEqNaturalNumberImpl<
    A extends NaturalNumber,
    B extends NaturalNumber
> = {
    0 : true,
    1 : (
        LtDigit<A[0], B[0]> extends true ?
        true :
        A[0] extends B[0] ?
        LtEqNaturalNumberImpl<
            PopFront<A>,
            PopFront<B>
        > :
        false
    ),
    2 : never
}[
    A["length"] extends 0 ?
    0 :
    number extends A["length"] ?
    2 :
    1
];
type LtEqNaturalNumber<
    A extends NaturalNumber,
    B extends NaturalNumber
> = (
    LtEqNaturalNumberImpl<
        LeftPad<A, 0, LongerTuple<A, B>["length"]>,
        LeftPad<B, 0, LongerTuple<A, B>["length"]>
    > extends infer X ?
    (
        X extends boolean ?
        X :
        never
    ) :
    never
);

//false
type ltEqNaturalNumber_0 = LtEqNaturalNumber<
    [1],
    [0]
>;
//true
type ltEqNaturalNumber_1 = LtEqNaturalNumber<
    [5,2,3],
    [4,8,9,2,3]
>;
//false
type ltEqNaturalNumber_2 = LtEqNaturalNumber<
    [4,8,9,2,3],
    [5,2,3]
>;
//true
type ltEqNaturalNumber_3 = LtEqNaturalNumber<
    [5,2,3],
    [5,2,3]
>;
//true
type ltEqNaturalNumber_4 = LtEqNaturalNumber<
    [5,2,2],
    [5,2,3]
>;
//false
type ltEqNaturalNumber_5 = LtEqNaturalNumber<
    [5,1],
    [2,5]
>;
//false
type ltEqNaturalNumber_6 = LtEqNaturalNumber<
    [2,5,7],
    [2,5,6]
>;

type RangeLt<N extends NaturalNumber> = (
    number &
    {
        readonly __rangeLt : N|undefined;
    }
);
type StringLengthLt<N extends NaturalNumber> = (
    string & { length : RangeLt<N> }
);

type AssertStringLengthLt<S extends StringLengthLt<NaturalNumber>, N extends NaturalNumber> = (
    LtEqNaturalNumber<
        Exclude<S["length"]["__rangeLt"], undefined>,
        N
    > extends true ?
    S :
    CompileError<[
        "Expected string of length less than",
        N,
        "received",
        Exclude<S["length"]["__rangeLt"], undefined>
    ]>
);
/**
 * String of length less than 256
 */
type StringLt256 = string & { length : RangeLt<[2,5,6]> };
/**
 * String of length less than 512
 */
type StringLt512 = string & { length : RangeLt<[5,1,2]> };

declare function foo<S extends StringLengthLt<NaturalNumber>> (
    s : AssertStringLengthLt<S, [2,5,6]>
) : void;

declare const str256 : StringLt256;
declare const str512 : StringLt512;

foo(str256); //OK!
foo(str512); //Error

declare function makeLengthRangeLtGuard<N extends NaturalNumber> (...n : N) : (
    (x : string) => x is StringLengthLt<N>
);

if (makeLengthRangeLtGuard(2,5,6)(str512)) {
    foo(str512); //OK!
}

declare const blah : string;
foo(blah); //Error

if (makeLengthRangeLtGuard(2,5,5)(blah)) {
    foo(blah); //OK!
}

if (makeLengthRangeLtGuard(2,5,6)(blah)) {
    foo(blah); //OK!
}

if (makeLengthRangeLtGuard(2,5,7)(blah)) {
    foo(blah); //Error
}

Детская площадка

Отсюда он использует тип CompileError<> ,
https://github.com/microsoft/TypeScript/issues/23689#issuecomment -512114782

Тип AssertStringLengthLt<> - вот где происходит волшебство

Используя сложение на уровне типов, вы можете получить str512 + str512 и получить str1024

https://github.com/microsoft/TypeScript/issues/14833#issuecomment -513106939

Глядя на решение @AnyhowStep, я думаю, что у меня есть лучшее временное решение. Помните типа охранников ?:

/**
 * Just some interfaces
 */
interface Foo {
    foo: number;
    common: string;
}

interface Bar {
    bar: number;
    common: string;
}

/**
 * User Defined Type Guard!
 */
function isFoo(arg: any): arg is Foo {
    return arg.foo !== undefined;
}

/**
 * Sample usage of the User Defined Type Guard
 */
function doStuff(arg: Foo | Bar) {
    if (isFoo(arg)) {
        console.log(arg.foo); // OK
        console.log(arg.bar); // Error!
    }
    else {
        console.log(arg.foo); // Error!
        console.log(arg.bar); // OK
    }
}

doStuff({ foo: 123, common: '123' });
doStuff({ bar: 123, common: '123' });

Взгляните на следующий код:

class NumberRange {
    readonly min: number;
    readonly max: number;
    constructor(min:number, max:number, ) {
        if (min > max) { 
            throw new RangeError(`min value (${min}) is greater than max value (${max})`);
        } else {
            this.min = min;
            this.max = max;
        }
    }
    public isInRange = (num: number, explicit = false): boolean => {
        let inRange: boolean = false;
        if (explicit === false) {
            inRange = num <= this.max && num >= this.min;
        } else {
            inRange = num < this.max && num > this.min;
        }
        return inRange;
    };
}
const testRange = new NumberRange(0, 12);
if(testRange.isInRange(13)){
    console.log('yay')
}else {
  console.log('nope')
}

Это просто идея, но с защитой типа можно использовать диапазоны.

Проблема здесь в том, что вы не можете этого сделать,

declare const a : number;
declare let b : (>= 5);
const testRange = new NumberRange(12, 20);
if (testRange.isInRange(a)) {
  b = a; //ok
} else {
  b = a; //compile error
}

Сами по себе защитные ограждения не являются удовлетворительным решением.

Кроме того, в вашем примере даже не используются охранники типа.


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


Кроме того, типы тегов, номинальные типы и объекты значений, используемые в примитивах, обычно являются признаком того, что система типов недостаточно выразительна.

Я ненавижу свой пример с глупой игрушкой, в которой используются типы тегов, потому что он крайне неэргономичен. Вы можете обратиться к этому пространному комментарию, чтобы узнать, как лучше использовать диапазоны в качестве примитивов.

https://github.com/microsoft/TypeScript/issues/6579#issuecomment -548249683

Вы можете добиться этого с помощью текущей версии Typescript:

// internal helper types
type IncrementLength<A extends Array<any>> = ((x: any, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never;
type EnumerateRecursive<A extends Array<any>, N extends number> = A['length'] extends infer X ? (X | { 0: never, 1: EnumerateRecursive<IncrementLength<A>, N> }[X extends N ? 0 : 1]) : never;

// actual utility types
export type Enumerate<N extends number> = Exclude<EnumerateRecursive<[], N>, N>;
export type Range<FROM extends number, TO extends number> = Exclude<Enumerate<TO>, Enumerate<FROM>>;

// usage examples:
type E1 = Enumerate<3>; // hover E1: type E1 = 0 | 1 | 2
type E2 = Enumerate<10>;  // hover E2: type E2 = 0 | 1 | 3 | 2 | 4 | 5 | 6 | 7 | 8 | 9

type R1 = Range<0, 5>; // hover R1: type R1 = 0 | 1 | 3 | 2 | 4
type R2 = Range<5, 11>; // hover R2: type R2 = 10 | 5 | 6 | 7 | 8 | 9

Я следовал соглашению о включении начального и исключительного конечных индексов, но вы можете настроить его под свои нужды.

Подсказки по коду vs в комментариях.

Обратите внимание, что числа в подсказках отсортированы случайным образом.

image

К сожалению, он работает только с 10 элементами :(

Изменить: кажется, что Enumerate может обрабатывать только до 15 рекурсий (0-14)

@ Shinigami92
При таком подходе Enumerate обрабатывает до 40.
Это все еще ограничение, но на практике оно может быть не таким жестким.

type PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T ? ((t: T, ...a: A) => void) extends ((...x: infer X) => void) ? X : never : never;
type EnumerateInternal<A extends Array<unknown>, N extends number> = { 0: A, 1: EnumerateInternal<PrependNextNum<A>, N> }[N extends A['length'] ? 0 : 1];
export type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[] ? E : never;
export type Range<FROM extends number, TO extends number> = Exclude<Enumerate<TO>, Enumerate<FROM>>;

type E1 = Enumerate<40>;
type E2 = Enumerate<10>;
type R1 = Range<0, 5>;
type R2 = Range<5, 34>;

И вот случилось как-то волшебным образом перебрать;).

В типичных случаях использования для меня используются диапазоны, такие как [1, 255] , [1, 2048] , [1, 4096] , [20, 80] и т. Д. Создание больших типов объединения может заставить TS нервничать / замедляться. . Но эти решения определенно работают для "меньших" диапазонов.

@AnyhowStep
Зная, что количество рекурсий является пределом, мы должны найти способ выполнять деление / умножение чисел на два в одной нерекурсивной операции внутри определения типа для достижения этих диапазонов.

Производительность по-прежнему остается проблемой. По этой причине мне раньше приходилось отбирать полезные типы объединения из больших приложений - хотя это может быть интересным упражнением, это определенно не решение.

Думаю, до сих пор нет идеальных решений?

Я надеюсь, что это можно сделать следующим образом (хотя и сложно).

type X => (number >= 50 && number > 60 || number = 70) || (string.startsWith("+"))

(т.е. здесь можно использовать обычный оператор / функцию javascript, за исключением того, что переменные заменяются типом)

В моем понимании современный язык = нормальная логика + мета-логика, где мета-логика = проверка кода + генерация кода. Многие работы просто пытаются элегантно объединить синтаксис мета-логики.

Может быть, мы можем каким-то образом предположить, что типы диапазонов - это не какой-то сахар, а основная концепция?

// number literal extend `number` despite the fact 
// that `number` is not union of all number literals
type NumberLiteralIsNumber = 5 extends number ? true : false // true

// so if we will define Range (with some non-existing syntax which is clearly should be done in lib internals)
type Range<MIN extends number, MAX extends number> = MAX > MIN ? 5 and 2NaN : MAX === MIN ? MAX : MIN..MAX

// and assume that `number` is Range<-Infinity, +Infinity>
type NumberIsInfinitRange = number extends Range<-Infinity, +Infinity> ?
    Range<-Infinity, +Infinity> extends number ? true : false :
    false // true

// following things will be true
type AnyRangeIsNumber<T extends number, K extends Number> = 
    Range<T, K> extends number ? true : false // true
type NestedRangeIsNumber<T extends number, K extends number, S extends number> =
    Range<T, Range<K, S>> extends number ? true : false // true

Чтобы завершить это, нам нужно объявить поведение в некоторых случаях

  1. Range<T, T> === T по определению
  2. Range<5, 1> === NaN по определению
  3. Range<NaN, T> === Range<T, NaN> === NaN поскольку такое значение не может существовать
  4. Range<1 | 2, 5> === Range<2, 5> возможность большего числа в качестве параметра первого типа ограничивает диапазон, чтобы он был короче
  5. Range<1, 4 | 5> === Range<1, 4> возможность меньшего числа в качестве параметра второго типа ограничивает диапазон, чтобы он был короче
  6. 1.5 extends Range<1, 2> должно быть истинным по определению
  7. Range<Range<A, B>, C> === NaN extends Range<A, B> ? NaN : Range<B, C> следует из 5 и 3
  8. Range<A, Range<B, C>> === NaN extends Range<B, C> ? NaN : Range<A, B> следует из 6 и 3

И немного о диапазонах внутри диапазонов:

type RangeIsInsideRange<T extends Range<any, any>, K extends Range<any, any>> = 
    T extends Range<infer A, infer B> 
        ? K extends Range<infer C, infer D> 
            ? NaN extends Range<A, C> 
                ? false 
                : NaN extends Range<B, D> 
                    ? false 
                    : true 
            : never
        : never

Подтипы диапазонов могут быть даже определены в общем виде, как функция сравнения на уровне типов (a: T, b: T) => '<' | '=' | '>' .

... есть ли предложение использовать обычные функции JS на уровне типов?

По сути, предложения нет, но я уже написал быстрый прототип раньше . К сожалению, есть большие опасения по поводу производительности IDE и очистки ввода.

Проблема @yudinns в том, что Range существует по крайней мере под этим именем, поэтому это может сбивать с толку, см. https://developer.mozilla.org/en-US/docs/Web/API/range, также мне не хватает функциональности, где я могу определить числовой ряд вроде <1, n+2> - шаг 2, начиная с 1 или более сложного уравнения.

Это предложение TC39 , вероятно, достигнет стадии 4 до того, как оно будет реализовано.
Билету 3 года.

Предложение: проверенный регулярным выражением тип строки: https://github.com/Microsoft/TypeScript/issues/6579

(связанный вопрос SO: https://stackoverflow.com/questions/3895478/does-javascript-have-a-method-like-range-to-generate-a-range-within-the-supp)

Также было бы здорово, если бы вы могли сделать что-то вроде number in range например if 1 in 1..2 или включительно if 1 in 1..=2 rust хорошо справляется с этим https://doc.rust-lang.org/reference/ выражения / диапазон-expr.html . Это сэкономит много места

Разве в Python и Rust уже нет функции типизированного диапазона?
Можем ли мы просто переопределить существующие решения на других языках вместо того, чтобы изобретать колесо или делать его более универсальным со строками, числами с плавающей запятой и т. Д., В то время как основная цель и самый большой вариант использования - это диапазоны чисел. Мы можем добавить больше позже, когда понадобится

Я сталкивался с этим не один раз и пришел сюда, чтобы найти способ определить тип, который представляет собой дробь между 0 ... 1 - это обсуждается на https://news.ycombinator.com/item?id= 24362658 # 24372935, потому что в английском языке нет правильного слова для определения этих типов значений. Если машинописный текст может помочь здесь, это было бы отлично, но это может быть чрезвычайно сложно реализовать на уровне типа.

@andrewphillipo Я видел, что это тоже обсуждается, и узнал о термине « Единичный интервал» из одного из ответов об обмене стеками, что, по-видимому, является правильным способом ссылки на этот конкретный диапазон в общем контексте.

Если диапазоны как типы когда-либо будут реализованы в машинописном тексте, возможно, UnitInterval можно было бы определить как глобальный тип, чтобы стимулировать некоторую общую номенклатуру вокруг того, что мы часто используем в программировании.

Да UnitInterval - правильное имя для диапазона 0 ... 1, поэтому оно подходит для данного типа! По-прежнему не подходит для наименования числа, поэтому было бы отлично, если бы это было доступно для более точного описания нашего кода с использованием такого типа - как это работает под капотом в системе типов Rust - это просто охранник или?

Если CantorSpace не слишком натянуто, я думаю, это было бы правильным определением «диапазона» «всех» действительных чисел между [0, 1] как их понимает компьютер. Присвоение им значений во время компиляции не может быть выведено с помощью нижней и верхней границы Math.floor или Math.ceil в javascript, поскольку Math.ceil(0) === 0 , и Math.floor(1) === 1 что является несчастный.

Если бы вместо этого использовался набор всех чисел (0, 1) , он работал бы с использованием приведенного выше примера, но это плохо исключать значения, упоминаемые как проценты в повседневном языке. Если возможно, было бы неплохо, если бы компилятор каким-то образом включил границы, возможно, с помощью строгой проверки === на крайние случаи 0 и 1.

или Может быть, использовать 1~~3 для целых чисел и 0.1~0.5 для чисел с плавающей запятой?

~ уже занято унарным побитовым оператором НЕ .

Почему нам нужно говорить об этих чертовых поплавках, у нас проблема с целыми числами. 99,99% - проблема с целыми числами. 0,001% - проблема с плавающей точкой.
Легко не найти общего решения, поэтому давайте выберем диапазон чисел от 0 до 10, как это видно на большинстве языков программирования, как это уже было реализовано много лет назад.

Перестань говорить

Возможно, мы сможем разделить счет и расширить это предложение для случая int:
https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c

Что касается поплавков, я сейчас работаю над другим предложением по дизайну, основанным на некоторых идеях из этого выпуска (в первую очередь @AnyhowStep; в основном возможность иметь Open-Interval-Type).

Просто будь проще с
x..y для целых чисел.
0.1..2 выдаст ошибку
1..2.1 также выдаст ошибку
И любое обнаруживаемое не целое число.
Черт возьми, скопируйте логику реализации из
https://en.m.wikibooks.org/wiki/Ada_Programming/Types/range
Или
https://kotlinlang.org/docs/reference/ranges.html
Или
https://doc.rust-lang.org/reference/expressions/range-expr.html
и закончить день.

✅ Не нужно изобретать велосипед
✅Нет необходимости учитывать поплавки
✅Согласованность на разных языках
✅Протестирован и скопирован стабильный код из существующих источников

Попытки решить числа с плавающей запятой стоит усилий, поскольку проверка типа на основе "int" тривиальна для чисел <1000, жестко кодируя значение типа как объединение каждого фиксированного целого числа 0..1000, таким образом говоря, что нет необходимости "повторно" «Изобрести колесо» - это немного иронично. Почему бы не использовать то, что уже работает, для простых случаев использования?

Я бы предпочел найти место для определения типа, которое понимает такие вещи, как иррациональные числа. Возможность обнаруживать инварианты во время компиляции при работе с радианами была бы интересным вариантом использования. Поддержка τ в качестве аргумента границы значения диапазона - хорошая цель, чтобы заявить, что эта функция работает так, как предполагалось.

Имейте в виду, что в javascript нет явного integer . Все просто number , которое хранится как число с плавающей запятой. Если реализован оператор типа диапазона, он должен обрабатывать любое значение, которое может обработать number .

@aMoniker в зависимости от определения explicit : BigInt

Даже для bigint есть случаи, когда союзы не могут решить, например, убедиться, что bigint помещается в MySQL signed / unsigned bigint.

Как насчет создания типа диапазона intrinsic для этой функции?

lib.es5.d.ts (или lib.es2020.bigint.d.ts для включения поддержки bigints)
type GreaterThan<N extends number | bigint> = intrinsic
type GreaterThanOrEqualTo<N extends number | bigint> = GreaterThan<N> | N
type LessThan<N extends number | bigint> = intrinsic
type LessThanOrEqualTo<N extends number | bigint> = LessThan<N> | N

/**
 * prevent `GreaterThan` and `LessThan` from desugaring
 * (in the same way that `number` does _not_ desugar to `-Infinity | ... | Infinity`)
 */
пользовательское пространство
type GreaterThanOrEqualTo2 = GreaterThanOrEqualTo<2>
type LessThan8 = LessThan<8>

type GreaterThanOrEqualTo2_And_LessThan8 = GreaterThanOrEqualTo2 & LessThan8
type LessThan2_Or_GreaterThanOrEqualTo8 = LessThan<2> | GreaterThanOrEqualTo<8> // inverse of `GreaterThanOrEqualTo2_And_LessThan8` (would be nice to be able to just do `Exclude<number | bigint, GreaterThanOrEqualTo2_And_LessThan8>` but that might be wishful thinking)
type LessThan2_And_GreaterThanOrEqualTo8 = LessThan<2> & GreaterThanOrEqualTo<8> // `never`
type GreaterThanOrEqualTo2_Or_LessThan8 = GreaterThanOrEqualTo2 | LessThan8 // `number | bigint`

type RangesAreNumbersOrBigIntsByDefault = LessThan8 extends number | bigint ? true : false // `true` (the user could always narrow this on a per-range basis, e.g. `LessThan8 & number`)
type RangesAcceptUnions = LessThan<7n | 7.5 | 8> // `LessThan<8>`
type RangesAcceptOtherRanges1 = LessThan<LessThan8> // `LessThan8`
type RangesAcceptOtherRanges2 = LessThan<GreaterThanOrEqualTo2> // `number | bigint`
type RangesSupportBeingInAUnion = (-6 | 0.42 | 2n | 2 | 3) | LessThan<2> // `LessThan<2> | 2n | 2 | 3`
type RangesSupportBeingInAnIntersection = (-6 | 0.42 | 2n | 2 | 3) & LessThan<2> // `-6 | 0.42`
type RangesSupportBeingInAUnionWithOtherRanges = LessThan<2> | LessThan8 // `LessThan8`
type RangesSupportBeingInAnIntersectionWithOtherRanges = LessThan<2> & LessThan8 // `LessThan<2>`

@RyanCavanaugh

Для этого действительно нужны убедительные примеры использования, потому что реализация союзов сегодня совершенно не подходит для реализации таких больших союзов. Что-то, что случилось бы, если бы вы написали что-то вроде этого ...

Следующее будет неверным без типа int:

const arr: string[] = ['a', 'b', 'c']
const item = arr[1.01]  // Typescript inferred this as string but actually it is undefined
console.log(item)  // Will print undefined, we miss inferred the type

Наличие следующего типа предотвратит эту проблему:

type TUInt = 0..4294967295;

Другой вариант использования типа Int32 и Int64 - это когда люди начинают аннотировать свой код с его помощью ... откроют дверь для лучшей совместимости с другими языками ... почти все статически типизированы языки имеют целочисленный тип: Java, C #, C, Rust, F #, Go ... и т. д.

Если я хочу вызвать библиотеку npm, написанную на TypeScript, из C #, например, есть библиотеки, которые принимают определения TypeScript и создают для меня интерфейс на C #, но проблема в том, что number type - float который нельзя использовать для индексации массива в C # без его корпуса ... и т. д.

Другие варианты использования: проще переносить между языками, оптимизировать производительность и т. Д.

Обратите внимание, что еще один случай, когда это было бы полезно, - это взаимодействие с WebAssembly, который имеет очень явные отдельные типы чисел (я предполагаю, что он был кратко упомянут как вариант использования C #, но хотел уточнить, что он применяется гораздо шире, чем это).

UPD: Nvm, я вижу, это было упомянуто в https://github.com/microsoft/TypeScript/issues/15480#issuecomment -365420315, который Github «услужливо» спрятал как «скрытые элементы».

+1

Была ли эта страница полезной?
0 / 5 - 0 рейтинги