Typescript: Sugerencia: rango como tipo de número

Creado en 30 abr. 2017  ·  106Comentarios  ·  Fuente: microsoft/TypeScript

Al definir un tipo, se pueden especificar varios números separados por | .

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

Permitir especificar tipos de números como rangos, en lugar de enumerar cada número:

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

Tal vez use .. para enteros y ... para flotantes.

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

Comentario más útil

Esta idea se puede expandir a caracteres, por ejemplo, "b".."d" sería "b" | "c" | "d" . Sería más fácil especificar conjuntos de caracteres.

Todos 106 comentarios

Esta idea se puede expandir a caracteres, por ejemplo, "b".."d" sería "b" | "c" | "d" . Sería más fácil especificar conjuntos de caracteres.

Creo que esto se puede expandir y usar sintaxis similar y semántica como rangos de Haskell.

| Sintaxis | Desaguiado |
| -------------------------- | ---------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------------------------------ |
| type U = (e1..e3) | type U = \| e1 \| e1+1 \| e1+2 \| ...e3 \|
La unión es never si e1 > e3 |
| type U2 = (e1, e2..e3) | type U2 = \| e1 \| e1+i \| e1+2i \| ...e3 \| ,
donde el incremento, i , es e2-e1 .

Si el incremento es positivo o cero, la unión termina cuando el siguiente elemento sería mayor que e3 ;
la unión es never si e1 > e3 .

Si el incremento es negativo, la unión termina cuando el siguiente elemento sería menor que e3 ;
la unión es never si e1 < e3 . |

@panuhorsmalahti ¿ "bb".."dd" ?

@streamich

Quizás use .. para enteros y ... para flotantes.

Realmente me gusta la idea de generar tipos integrales como este, pero no veo cómo podrían funcionar los valores de punto flotante.

@aluanhaddad Diga probabilidad:

type TProbability = 0.0...1.0;

@streamich para que ese tipo tenga un número teóricamente infinito de posibles habitantes?

@aluanhaddad en realidad estaría lejos de ser infinito en el punto flotante IEEE. Tendría 1.065.353.217 habitantes según mis cálculos.

0.0...1.0 ? JS usa IEEE doble, eso es 53 bits de rango dinámico. Si eso fuera compatible, los rangos tendrían que ser del tipo de primera clase, desugar eso para un sindicato sería más que impráctico.

@jcready, de hecho, pero, como señala @fatcerberus , darse cuenta de que es un tipo de unión sería prohibitivamente expansivo.

A lo que me refería, de manera indirecta, era a que esto introduciría alguna noción de tipos discretos versus continuos en el lenguaje.

realizarlo como un tipo de unión sería prohibitivamente expansivo.

@aluanhaddad Sí, pero incluso especificar un número entero sin firmar como unión sería muy caro:

type TUInt = 0..4294967295;

Esto realmente necesita algunos casos de uso convincentes, porque la implementación de uniones hoy en día es completamente inadecuada para realizar uniones de este tamaño. Algo que sucedería si escribieras algo como esto

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

sería la instanciación del tipo de unión 0 | 1 | 2 | 3 | 5 | 6 | 7 | ... .

Quizás solo podría funcionar contra números literales. Cualquier valor numérico no literal tendría que refinarse explícitamente con comparaciones mayor / menor que antes de que se considere que habita en el rango. Los rangos de enteros también requerirían una verificación adicional Number.isInteger() . Esto debería eliminar la necesidad de generar tipos de unión reales.

@RyanCavanaugh ¿Tipos de resta? 🌞

Tipos negativos, tipo negación.

Cualquier cosa menos una cuerda:

type NotAString = !string;

Cualquier número excepto cero:

type NonZeroNumber = number & !0;

Los tipos de resta de

Mi caso de uso es: me gustaría escribir un parámetro como 0 o un número positivo (es un índice de matriz).

@RoyTinker Definitivamente creo que esto sería genial, pero no sé si ese caso de uso ayuda a la discusión.
Una matriz es solo un objeto y los índices ascendentes son solo una convención.

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

por lo que, en última instancia, aún debe realizar la misma verificación

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

Sería muy útil para definir tipos como segundo, minuto, hora, día, mes, etc.

@ Frikki esas unidades se encuentran en un intervalo lo suficientemente limitado como para que sea práctico y prohibitivamente difícil escribirlas a mano.

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 Pero no int sin firmar:

type UInt = 0..4294967295;

meh, ¿qué tal un tipo como este:

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

Usando el operador * del comentario de @ aleksey-bykov:

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

@streamich Duplicar el recuento de bits no corresponde a la multiplicación por dos, es más como una exponencia con 2 como exponente. Sin embargo, todavía no es correcto, ya que no debe aumentar el límite superior, pero los números codificables cuentan. Con todo, esa no es una buena estrategia de definición.

@streamich , algunos comentarios:

  • El uso de los términos char , word , etc. podría resultar confuso, ya que las personas de otros idiomas podrían no darse cuenta de la diferencia entre la definición estática y el comportamiento en tiempo de ejecución.
  • La sintaxis propuesta no tiene en cuenta el límite inferior, ¿y si no es cero?
  • Sería cauteloso a la hora de utilizar el operador de potenciación para su uso en un contexto de tipo / ambiente, ya que ya se ha agregado a ES2016.

vamos a completar el turing del sistema de tipos y disfrutar del problema de la Ctrl + Shift + B

@ aleksey-bykov seguramente recuerdas este buen número 😀

@aluanhaddad

esas unidades [unidades de tiempo] se encuentran en un intervalo lo suficientemente limitado como para que sea práctico y prohibitivamente difícil escribirlas a mano.

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

Ahora, haz eso con milisegundos: guiño:

¿Este problema está muerto?

@Palid está etiquetado con [Needs Proposal] así que lo dudo.

Por muy divertida que haya sido la discusión, la mayoría de nosotros no hemos podido proporcionar casos prácticos convincentes del mundo real.

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

__USO DE CASOS: __

  1. Puede definir tipos de int precisos de modo que el subconjunto de TypeScript se pueda compilar en WebAssembly o en algún otro destino.

  2. Otro caso de uso es UInt8Array , Int8Array , Uint16Array , etc., cuando lee o escribe datos de esos TypeScript podría comprobar si hay errores.

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

uint8[0] = 0xFFFFFFFF; // TSError: Number too big!
  1. He mencionado algunos casos de uso en mi OP.

  2. Si implementa esto, la comunidad de TypeScript presentará millones de casos de uso.

Tengo un caso de uso realmente divertido, que en realidad puede estar más cerca de cualquier otra cosa que se proponga aquí.
La API toma un número entero entre algún rango (en mi caso 5-30) y necesitamos implementar un SDK para eso.
Escribir manualmente debajo es tedioso (aunque puede ser algo automatizado) para 25 valores, pero ¿qué pasa con cientos o miles?
type X = 5 | 6 | 7 | 8 | 9 | 10 ... | 30

@Palid Ese es el mejor y más sencillo caso que he visto para esta función.

Si un rango como 5..30 se definió como azúcar sintáctico para 5 | 6 | 7 ... | 30 (o funciona de manera idéntica), apuesto a que esto sería una victoria fácil. Aquí representaría un rango discreto de números enteros.

Quizás un rango continuo (a diferencia de discreto) podría anotarse usando un número con un punto - 5.0..30.0 .

De hecho, estaba considerando buscar en TypeScript para implementar la protección de escritura para
https://www.npmjs.com/package/memory-efficient-object

Tener la escritura de rango facilitaría la detección de un posible desbordamiento de memoria en el momento de la verificación de tipos

Parece que realmente no necesitamos la expansión de la unión, sino más bien una definición de rango inclusiva / exclusiva que solo compara el rango

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

Esto sería muy útil al controlar motores (usando Johnny-Five, por ejemplo), donde la velocidad varía de 0-255.

Otro caso de uso: estoy implementando un programa de dibujo basado en Canvas usando TypeScript y me gustaría tener verificaciones de tipo en la opacidad (que debe ser un número entre 0.0 y 1.0).

Solo pensando ... para implementar esto correctamente, tendrías que hacer todo lo posible:

  • admite funciones de protección de tipo de tiempo de ejecución
  • estrechamiento de tipos para condiciones como x <= 10
  • soporte para rangos de string | number (ya que x == 5 es true por x === "5" ) además de rangos de solo números
  • probablemente incluso soporte para un nuevo tipo int (que sería un subtipo de number ), y soporte para rangos int -only y string | int . También escriba el estrechamiento para expresiones como x|0

¡Gran idea, pero eso sería mucho para cubrir!

Tal vez no necesitemos un tipo de protección en tiempo de ejecución. En su lugar, podríamos hacer que el tiempo de compilación se guarde hasta el final

En su lugar, esto necesitaría una predicción de rama elaborada

Suponer

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

Mi caso de uso:

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[] },
  // ...
}

Sería bueno poder restringir las claves en la opción statusCode modo que en tiempo de compilación se pueda determinar si un código de estado corresponde a un código de éxito o error:

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

En mi opinión, esto debería restringirse a números flotantes y enteros y no debería implementarse como una unión, sino como un nuevo tipo de rango. Entonces, la verificación de tipos sería tan simple como:

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

Quizás podríamos tomar una hoja del libro de Ruby y usar .. para rangos inclusivos ( [start, stop] ) y ... para rangos no inclusivos ( [start, stop) ).

Otro caso de uso sería verificar los registros de su base de datos:

Otro caso de uso sería la verificación de tipos de valores Lat / Lon.

Esto podría estar cubierto por el mecanismo de validación general de # 8665 (no estoy seguro de por qué se cerró como duplicado):

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;

O tomado con # 4639, y asumiendo que el tipo de entero sin signo se define como uint :

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

Mi caso de uso son las coordenadas globales. Quiero escribir la latitud y la longitud para que solo estén dentro de los rangos específicos (-90 a 90 y -180 a 180).

Editar: lat y long tienen un rango negativo

Mi caso de uso es la implementación de matrices de tamaño fijo donde el tamaño es un parámetro.

Por ejemplo, quiero definir un tipo para matrices de cadenas de longitud 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

Con la versión actual de TS es posible definir algo muy parecido a la "especificación" anterior. El principal problema es el acceso de miembros: por ejemplo, d[1] = 'b1' devuelve un error de tipo incluso si es correcto. Para evitar el error, la lista de todos los índices legales debe compilarse a mano en la definición de FixedSizeArray , que es aburrida.

Si tuviéramos un operador range similar al operador keyof , la siguiente definición de tipo debería resolver el problema.

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

Donde range(N) es un atajo para range(0,N) .

Dados literales numéricos (naturales) M, N con M <N,

type r = range(M, N); 

es equivalente a

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

Lo que podría ser más general es si pudiéramos tener la capacidad de definir lambdas predicados y usarlos como tipos. Ejemplo:

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

Un profesional sería una baja complejidad de sintaxis ya que las Lambdas ya están disponibles, todo lo que necesitamos es la capacidad de usarlas como tipos, la verificación de tipos es específica de mecanografiado de todos modos (teniendo en cuenta la filosofía de "superconjunto a Javascript")
Una desventaja es que la complejidad del predicado determinará el rendimiento de las herramientas para proporcionar retroalimentación.

Asegurarse de que un número / carácter pertenece a una progresión aritmética podría ser un buen caso de uso común:

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

esto nos permitiría hacer comprobaciones de tipo como:
predicado perteneceToMyAP = perteneceToAP (1,1, 10)

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

Esto también se puede expandir a personajes.

@Kasahs Algo similar a esto ya se ha propuesto en el # 8665.

Poniendo mi suerte en el caso de uso de "reflejar una API". Un punto final REST para el que estoy escribiendo una función contenedora toma un número entero en el rango 1..1000 como argumento. Genera un error si el número no satisface esa restricción.

Así que estoy escribiendo una propuesta para rangos numéricos y me he encontrado con este problema que no estoy seguro de cómo manejarlo, así que lo estoy lanzando para su consideración.

Debido a que el compilador de TypeScript está escrito en TypeScript, es posible aprovechar las propiedades de los operadores numéricos para mutar los tipos de rango.

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

Esto está bien cuando se asigna a una nueva variable, pero ¿qué pasa con la mutación?

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

Este error sería técnicamente correcto, pero en la práctica sería increíblemente molesto lidiar con él. Si queremos mutar una variable devuelta por una función con un tipo de retorno de rango, siempre tendríamos que agregar una aserción de tipo.

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.

Y si lo ignoramos, obtenemos la siguiente falta de solidez:

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.

Una solución para esto sería la capacidad de cambiar el tipo de una variable después de que se declara, por lo que el ejemplo 2 simplemente cambiaría el tipo de x a 2..12; pero mi primer instinto es que introduciría demasiada sobrecarga en el compilador y sería confuso para los usuarios.

¿Y qué hay de las funciones definidas por el usuario y los genéricos?

// 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);

¿Alguna idea sobre cómo lidiar con lo anterior?

@JakeTunaley

¿qué pasa con la mutación?

¿Por qué la mutación debería ser diferente a cualquier otra asignación?

Aquí está mi humilde intento de propuesta. Esta propuesta también solicita que se agreguen otros tipos.

  • Infinity tipo

    • Solo tiene el valor Infinity

  • -Infinity tipo

    • Solo tiene el valor -Infinity

  • NaN tipo

    • Solo tiene el valor NaN

  • double tipo

    • Todos los valores number en el rango [-Number.MAX_VALUE, Number.MAX_VALUE] o, [-1.7976931348623157e+308, 1.7976931348623157e+308]

  • number es solo Infinity|-Infinity|NaN|double
  • int tipo

    • Un subtipo de double

    • Todos los valores number x en el rango [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] o [-9007199254740991, 9007199254740991] y Math.floor(x) === x

    • Entonces, 3 y 3.0 serían los valores de un tipo int

  • Tipo "literal finito"

    • Algunos ejemplos son 1 , 3.141 , 45

    • Pueden ser subtipos de double o int

  • El tipo "GtEq" denota (>= x)

    • Donde x es un literal finito, Infinity o -Infinity

  • El tipo "LtEq" denota (<= x)

    • Donde x es un literal finito, Infinity o -Infinity

  • El tipo "Gt" denota (> x)

    • Donde x es un literal finito, Infinity o -Infinity

  • El tipo "Lt" denota (< x)

    • Donde x es un literal finito, Infinity o -Infinity


Tipo GtEq; (>= x)

  • (>= Infinity) = Infinity
  • (>= -Infinity) = -Infinity|double|Infinity
  • Infinity es un subtipo de (>= [finite-literal])
  • (>= [finite-literal]) es un subtipo de double|Infinity
  • (>= NaN) = never
  • (>= int) = (>= -9007199254740991)
  • (>= double) = (>= -1.7976931348623157e+308)
  • (>= number) = number

Tipo GT; (> x)

  • (> Infinity) = never
  • (> -Infinity) = double|Infinity
  • Infinity es un subtipo de (> [finite-literal])
  • (> [finite-literal]) es un subtipo de double|Infinity
  • (> NaN) = never
  • (> int) = (> -9007199254740991)
  • (> double) = (> -1.7976931348623157e+308)
  • (> number) = number

Tipo LtEq; (<= x)

  • (<= Infinity) = -Infinity|double|Infinity
  • (<= -Infinity) = -Infinity
  • -Infinity es un subtipo de (<= [finite-literal])
  • (<= [finite-literal]) es un subtipo de -Infinity|double
  • (<= NaN) = never
  • (<= int) = (<= 9007199254740991)
  • (<= double) = (<= 1.7976931348623157e+308)
  • (<= number) = number

Tipo Lt; (< x)

  • (< Infinity) = -Infinity|double
  • (< -Infinity) = never
  • -Infinity es un subtipo de (< [finite-literal])
  • (< [finite-literal]) es un subtipo de -Infinity|double
  • (< NaN) = never
  • (< int) = (< 9007199254740991)
  • (< double) = (< 1.7976931348623157e+308)
  • (< number) = number

Tipos de rango

Tenga en cuenta que si bien podemos escribir cosas como (>= Infinity) , (> number) , etc.,
el tipo resultante no es un tipo de rango; son solo alias para otros tipos.

Un tipo de rango es uno de,

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

Permitimos sintaxis como (> number) y similares para su uso en genéricos.


Tipos Union GtEq / Gt

Al tomar la unión de dos tipos GtEq / Gt, el tipo con "más" valores es el resultado,

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

    • Si [finite-literal-A] >= [finite-literal-B] , entonces el resultado es (>= [finite-literal-B])

    • De lo contrario, el resultado es (>= [finite-literal-A])

    • por ejemplo, (>= 3) | (>= 5.5) = (>= 3) porque (>= 3) es un supertipo de (>= 5.5)

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

    • Si [finite-literal-A] == [finite-literal-B] , entonces el resultado es (>= [finite-literal-A])

    • Si [finite-literal-A] > [finite-literal-B] , entonces el resultado es (> [finite-literal-B])

    • De lo contrario, el resultado es (>= [finite-literal-A])

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

    • Si [finite-literal-A] >= [finite-literal-B] , entonces el resultado es (> [finite-literal-B])

    • De lo contrario, el resultado es (> [finite-literal-A])

    • por ejemplo, (> 3) | (> 5.5) = (> 3) porque (> 3) es un supertipo de (> 5.5)

También,

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

    • por ejemplo, (> 4|3) = (> 4) | (> 3) = (> 3)

    • por ejemplo, (> number|3) = (> number) | (> 3) = number | (> 3) = number

Tipos Union LtEq / Lt

Al tomar la unión de dos tipos LtEq / Lt, el tipo con "más" valores es el resultado,

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

    • Si [finite-literal-A] <= [finite-literal-B] , entonces el resultado es (<= [finite-literal-B])

    • De lo contrario, el resultado es (<= [finite-literal-A])

    • por ejemplo, (<= 3) | (<= 5.5) = (<= 5.5) porque (<= 5.5) es un supertipo de (<= 3)

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

    • Si [finite-literal-A] == [finite-literal-B] , entonces el resultado es (<= [finite-literal-A])

    • Si [finite-literal-A] < [finite-literal-B] , entonces el resultado es (< [finite-literal-B])

    • De lo contrario, el resultado es (<= [finite-literal-A])

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

    • Si [finite-literal-A] <= [finite-literal-B] , entonces el resultado es (< [finite-literal-B])

    • De lo contrario, el resultado es (< [finite-literal-A])

    • por ejemplo, (< 3) | (< 5.5) = (< 5.5) porque (< 5.5) es un supertipo de (< 3)

También,

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

    • por ejemplo, (< 4|3) = (< 4) | (< 3) = (< 4)

    • por ejemplo, (< number|3) = (< number) | (< 3) = number | (< 3) = number


Tipos de intersección GtEq / Gt

Al tomar la intersección de dos tipos GtEq / Gt, el tipo con "menos" valores es el resultado,

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

    • Si [finite-literal-A] >= [finite-literal-B] , entonces el resultado es (>= [finite-literal-A])

    • De lo contrario, el resultado es (>= [finite-literal-B])

    • por ejemplo, (>= 3) & (>= 5.5) = (>= 5.5) porque (>= 5.5) es un subtipo de (>= 3)

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

    • Si [finite-literal-A] == [finite-literal-B] , entonces el resultado es (> [finite-literal-B])

    • Si [finite-literal-A] > [finite-literal-B] , entonces el resultado es (>= [finite-literal-A])

    • De lo contrario, el resultado es (> [finite-literal-B])

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

    • Si [finite-literal-A] >= [finite-literal-B] , entonces el resultado es (> [finite-literal-A])

    • De lo contrario, el resultado es (> [finite-literal-B])

    • por ejemplo, (> 3) & (> 5.5) = (> 5.5) porque (> 5.5) es un subtipo de (> 3)

Tipos de intersección LtEq / Lt

Al tomar la intersección de dos tipos LtEq / Lt, el tipo con "menos" valores es el resultado,

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

    • Si [finite-literal-A] <= [finite-literal-B] , entonces el resultado es (<= [finite-literal-A])

    • De lo contrario, el resultado es (<= [finite-literal-B])

    • por ejemplo, (<= 3) & (<= 5.5) = (<= 3) porque (<= 3) es un subtipo de (<= 5.5)

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

    • Si [finite-literal-A] == [finite-literal-B] , entonces el resultado es (< [finite-literal-B])

    • Si [finite-literal-A] < [finite-literal-B] , entonces el resultado es (<= [finite-literal-A])

    • De lo contrario, el resultado es (< [finite-literal-B])

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

    • Si [finite-literal-A] <= [finite-literal-B] , entonces el resultado es (< [finite-literal-A])

    • De lo contrario, el resultado es (< [finite-literal-B])

    • por ejemplo, (< 3) & (< 5.5) = (< 3) porque (< 3) es un subtipo de (< 5.5)


Casos de uso

  • Para garantizar estáticamente que un entero puede caber en un tipo de datos MySQL UNSIGNED INT ,

    //TODO Propose numeric and range sum/subtraction/multiplication/division/mod/exponentiation types?
    function insertToDb (x : int & (>= 0) & (<= 4294967295)) {
      //Insert to database
    }
    
  • Para garantizar estáticamente que una cadena tiene una longitud determinada,

    function foo (s : string & { length : int & (>= 1) & (<= 255) }) {
      //Do something with this non-empty string that has up to 255 characters
    }
    
  • Para garantizar estáticamente que un objeto similar a una matriz tenga el length apropiado,

    function foo (arr : { length : int & (>= 0) }) {
      //Do something with the array-like object
    }
    
  • Para asegurarnos estáticamente que solo se nos dan números finitos,

    function foo (x : double) {
      //`x` is NOT NaN|Infinity|-Infinity
    }
    
  • ¿Para garantizar estáticamente la existencia de índices de matriz?

    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
    }
    

Me encantaría proponer tipos de suma / resta / multiplicación / división / mod / exponenciación numérica y de rango, pero eso parece estar fuera de alcance con este problema.

[Editar]
Podrías cambiar el nombre de double y llamarlo float pero pensé que double representaba con mayor precisión que este es un número de punto flotante de doble precisión.

[Editar]

Se cambiaron algunos tipos a never .

¿Sería posible que el compilador hiciera el análisis de flujo?

Supongamos que existe esta función

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
}

un caso de uso más: tengo una entrada numérica para una función que representa un porcentaje. Quiero limitar los valores a estar entre 0 y 1.

Acabo de ejecutar [...Array(256)].map((_,i) => i).join("|") para hacer mi definición de tipo más fea hasta ahora

Son posibles números enteros no negativos y números pequeños:

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

Rango: https : //github.com/kgtkr/typepark/blob/master/src/list.ts

Por "números pequeños" te refieres sin usar la etapa 3 BigInt ?

@Mouvedia Un número que se ajusta al límite de recursividad del compilador

Quizás use .. para enteros y ... para flotantes.

Yo diría que .. debería significar un rango inclusivo y ... exclusivo. Como en, por ejemplo, Ruby. Ver http://rubylearning.com/satishtalim/ruby_ranges.html

No soy fanático de usar un solo período para diferenciar entre rangos inclusivos y exclusivos

¿Puedo saltar? Creo que esta característica vista como una mejora única para la especificación de tipo no es probablemente la mejor manera de hacerlo.

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

Así es como se define un rango numérico en muchas plataformas 4gl, específicamente las orientadas a matrices.

Lo hace de esta manera, porque un rango uniforme es convencionalmente uno de los mejores enfoques de modelado.

Entonces, un rango secuencial es este:

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

Y si solo los enteros:

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

// OR

type integers = number.integers<1:10>

Si queremos jugar con la sintaxis:

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

Si eso no es compatible con tokenizer, en mi opinión, esta no es la prioridad de bloqueo correcta en la que centrarse aquí. Señalo esto con la esperanza de que una solución no nos limite a la hora de llegar a la siguiente.

editar : Lo que no puedo tener en cuenta es el aspecto de inclusión, y aquí debemos preguntarnos si estamos haciendo la debida diligencia para comprender por qué eso no fue un problema cuando la gente resolvió tantos problemas confiando en rangos uniformes que incluyen implícitamente todos los números excepto cuando el incremento no se alineó exactamente con el final del rango.

Es interesante que mencione la posibilidad de definir tamaños de "pasos" en un tipo de rango.

Más allá de los rangos de "punto flotante" (sin tamaño de paso fijo) y "entero" (tamaño de paso de 1, comienza desde un número entero), nunca he encontrado un caso de uso de la vida real para rangos con otros tamaños de paso.

Entonces, es interesante escuchar que es algo en otros lugares. Ahora, tendré que aprender sobre 4gl porque nunca antes había oído hablar de él.


Ser capaz de definir un intervalo semiabierto es útil para objetos tipo matriz. Algo como,

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

Si solo tuviéramos rangos inclusivos, necesitaríamos (<= LengthT - 1) pero eso es menos elegante

¿Puedo saltar? Creo que esta característica vista como una mejora única para la especificación de tipo no es probablemente la mejor manera de hacerlo.

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

Así es como se define un rango numérico en muchas plataformas 4gl, específicamente las orientadas a matrices.

Lo hace de esta manera, porque un rango uniforme es convencionalmente uno de los mejores enfoques de modelado.

Entonces, un rango secuencial es este:

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

Y si solo los enteros:

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

// OR

type integers = number.integers<1:10>

Si queremos jugar con la sintaxis:

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

Si eso no es compatible con tokenizer, en mi opinión, esta no es la prioridad de bloqueo correcta en la que centrarse aquí. Señalo esto con la esperanza de que una solución no nos limite a la hora de llegar a la siguiente.

editar : Lo que no puedo tener en cuenta es el aspecto de inclusión, y aquí debemos preguntarnos si estamos haciendo la debida diligencia para comprender por qué eso no fue un problema cuando la gente resolvió tantos problemas confiando en rangos uniformes que incluyen implícitamente todos los números excepto cuando el incremento no se alineó exactamente con el final del rango.

Mmmm parece la forma en que se maneja en Haskell. Creo que esto es bueno. Los generadores también permitirán una evaluación perezosa.

No es que no puedas tener una evaluación perezosa con ninguna otra sintaxis = x

Es interesante que mencione la posibilidad de definir tamaños de "pasos" en un tipo de rango.

Pienso en un rango como inicio, final e incremento. Entonces, si el incremento se redondea exactamente al final, entonces incluye eso. Los índices de matriz y las longitudes, como son las funciones de rango, pueden ser una matriz 0: 1: 9 con 10 pasos de índice (longitud). Entonces, el rango aquí puede ser convenientemente integer & 0:9 para un sistema de tipos que se puede inferir más fácilmente al combinar esas dos expresiones.

4GL es realmente una etiqueta genérica, para mí era principalmente MatLab

Mi punto era que tener solo rangos inclusivos dificultaría su uso en genéricos (a menos que implemente las operaciones matemáticas de nivel de tipo hack-y).

Porque, en lugar de tener solo "longitud" como parámetro de tipo, ahora necesita longitud e índice máximo. Y el índice máximo debe ser igual a length-1.

Y, nuevamente, no puede verificar que ese sea el caso a menos que implemente esas operaciones matemáticas de nivel de tipo hack-y

@AnyhowStep Estaba pensando en la mejor manera de enmarcar su preocupación, pero tal vez ayude aclarar primero:

Si dejamos a un lado el inclusivo-o-no como específicamente aplicable a cualquier problema n-1 (como este escenario de índice / conteo), asumiendo que se resolvió de manera independiente de alguna manera (solo bromee la idea), ¿existen otros escenarios para los cuales los rangos numéricos serían todavía requiere una sintaxis menos declarativa y / o no convencional para describir correctamente?

Estoy llegando a esos 2 aspectos separados, necesita rangos numéricos que estén alineados convencionalmente con las expectativas de ese dominio y también necesita predicados para tipos de índice / conteo ... etc.

Solo números positivos.

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

Con rangos solo inclusivos,

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

Números positivos, excluyendo Infinity ,

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

Con rangos solo inclusivos,

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

Además, no está relacionado con la discusión actual, pero hay otra razón por la que realmente quiero tipos de rango numérico.

Gran parte de mi trabajo diario consiste en ensamblar varios sistemas de software diferentes. Y muchos de estos sistemas tienen diferentes requisitos para los datos que tienen el mismo significado.

por ejemplo, (sistemaA, valor entre 1 y 255), (sistemaB, valor entre 3 y 73), etc.
p. ej. (systemC. string length 7-88), (systemD, string length 9-99), (systemE, string length 2-101), etc.

En este momento, necesito documentar cuidadosamente todos estos requisitos separados y asegurarme de que los datos de un sistema se puedan asignar correctamente a otro sistema. Si no se asigna, necesito encontrar soluciones.

Soy sólo un humano. Yo cometo errores. No me doy cuenta de que los datos no se pueden asignar a veces. Las comprobaciones de rango fallan.

Con los tipos de rango numérico, finalmente puedo declarar los rangos que espera cada sistema y dejar que el compilador haga la verificación por mí.


Por ejemplo, acabo de tener una situación en la que la API que estaba usando tenía un límite de longitud de cadena de 10k para todos los valores de cadena. Bueno, no tenía forma de decirle a TypeScript que verificara que todas las cadenas que van a la API tienen una longitud de cadena <= 10k.

Tuve un error en tiempo de ejecución en lugar de un buen error de compilación donde TS podría ir,

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

@ AnyhowStep Espero que pueda apreciar que mi intención es simplemente asegurarme de que si algo llamado "Rango como tipo de número" simplemente se alinea con las expectativas comunes del usuario más convencional (es decir, alguien que se muda a TS y se pregunta por qué el rango enfatiza la inclusión sobre el intervalo)

Honestamente, creo que los casos de uso siempre deben impulsar las características, y creo que los problemas relacionados con el índice y la longitud son los que muchos de nosotros a veces realmente extrañamos. Así que simplemente quiero abordar ese problema justo debajo de la etiqueta correcta: ¿es una cosa de tipo numérico o una cosa de tipo indexado? No sé la respuesta exactamente, pero dudo en pensar que resolverlo como una cosa de tipo numérico no creará inadvertidamente muchos más problemas para los usuarios de tipo numérico que no comparten la misma comprensión de estos aspectos.

Entonces, ¿en qué otro lugar tendría sentido solucionar este problema para todos? Es todo lo que me pregunto en este momento, ¿pensamientos?

Estoy tratando con una API que pasa matrices de bytes, así que me gustaría definir un tipo de byte:

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

Esto también sería útil cuando se trabaja con Uint8Array .

Si eres como yo y estás impaciente por obtener tipos de rango que realmente puedas usar en este momento, aquí tienes un fragmento de código de los tipos de rango en acción.

TL; DR,
Los tipos de rango pueden funcionar ahora

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
}

Patio de recreo

Utiliza el tipo CompileError<> de aquí,
https://github.com/microsoft/TypeScript/issues/23689#issuecomment -512114782

El tipo AssertStringLengthLt<> es donde ocurre la magia

Usando la suma de nivel de tipo, puede tener str512 + str512 y obtener un str1024

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

Mirando la solución @AnyhowStep , creo que tengo una mejor solución temporal . ¿Recuerdas a los guardias de tipo ?:

/**
 * 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' });

Así que eche un vistazo al siguiente código:

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')
}

Es solo una idea, pero con los protectores de tipo es posible usar rangos.

El problema aquí es que no puedes hacer esto,

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
}

Los protectores de tipo por sí solos no son

Además, su ejemplo ni siquiera usa guardias de tipo.


Mi ejemplo de juguete hacky tonto anterior le permite asignar un tipo de rango a otro tipo de rango (a través de parámetros de función), si sus límites están dentro del otro.


Además, los tipos de etiquetas, los tipos nominales y los objetos de valor que se utilizan en primitivas suelen ser una señal de que el sistema de tipos no es lo suficientemente expresivo.

Odio mi ejemplo de juguete tonto que usa tipos de etiquetas porque es extremadamente poco ergonómico. Puede consultar este extenso comentario para saber cómo es mejor que los rangos sean primitivos.

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

Puede lograr esto con la versión actual de 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

Seguí la convención de tener índices de inicio inclusivos y finales exclusivos, pero puede ajustarlo a sus necesidades.

Las sugerencias de desplazamiento del código vs en los comentarios.

Observe que los números en las sugerencias de desplazamiento están ordenados aleatoriamente.

image

Lamentablemente, solo funciona hasta alrededor de 10 elementos :(

Editar: parece que Enumerate solo puede manejar hasta 15 recursiones (0-14)

@ Shinigami92
Con este enfoque, Enumerate maneja hasta 40.
Esto sigue siendo una limitación, pero puede que no sea tan severo en la práctica.

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

Y ahora pasó a ser ordenado de alguna manera mágicamente;).

Los casos de uso típicos para mí usan rangos como [1, 255] , [1, 2048] , [1, 4096] , [20, 80] , etc. La creación de tipos de unión grandes puede hacer que TS se asuste / ralentice . Pero esas soluciones definitivamente funcionan para rangos "más pequeños"

@AnyhowStep
Sabiendo que el recuento de recursividad es un límite, debemos encontrar la manera de realizar la división / multiplicación de números por dos en una sola operación no recursiva dentro de la definición de tipo para lograr estos rangos.

El rendimiento sigue siendo un problema. He tenido que seleccionar tipos de unión útiles de aplicaciones grandes antes por esta razón; aunque puede ser un ejercicio interesante, definitivamente no es una solución.

¿Todavía no hay soluciones perfectas, supongo?

Espero que esto se pueda hacer de la siguiente manera en general (aunque difícil).

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

(es decir, aquí se puede usar la declaración / función normal de JavaScript, excepto que las variables se reemplazan por el tipo)

A mi entender, lenguaje moderno = lógica normal + meta lógica, donde meta lógica = verificación de código + generación de código. Muchos trabajos solo intentan fusionar la sintaxis de la meta lógica de una manera elegante.

¿Quizás de alguna manera podemos asumir que los tipos de rango no son una especie de azúcar, sino un concepto central?

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

Para completar esto, necesitamos declarar el comportamiento en algunos casos.

  1. Range<T, T> === T por definición
  2. Range<5, 1> === NaN por definición
  3. Range<NaN, T> === Range<T, NaN> === NaN ya que dicho valor no puede existir
  4. Range<1 | 2, 5> === Range<2, 5> la posibilidad de un número mayor como primer parámetro de tipo restringe el rango para que sea más corto
  5. Range<1, 4 | 5> === Range<1, 4> la posibilidad de un número más bajo como segundo parámetro de tipo restringe el rango para que sea más corto
  6. 1.5 extends Range<1, 2> debería ser cierto por definición
  7. Range<Range<A, B>, C> === NaN extends Range<A, B> ? NaN : Range<B, C> sigue de 5 y 3
  8. Range<A, Range<B, C>> === NaN extends Range<B, C> ? NaN : Range<A, B> sigue de 6 y 3

Y un poco sobre rangos dentro de rangos:

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

Los subtipos de rango podrían incluso definirse de forma genérica, como una función de comparación de nivel de tipo (a: T, b: T) => '<' | '=' | '>' .

... ¿hay una propuesta para usar funciones JS regulares a nivel de tipo?

No hay propuesta, per sey, pero he escrito un prototipo rápido antes . Desafortunadamente, existen grandes preocupaciones sobre el rendimiento de IDE y la desinfección de entradas.

El problema de https://developer.mozilla.org/en-US/docs/Web/API/range también extraño la funcionalidad donde puedo definir series de números como <1, n+2> - paso 2 a partir de 1 o más ecuaciones complicadas.

Esta propuesta TC39 probablemente llegará a la etapa 4 antes de que se implemente.
El boleto tiene 3 años.

Sugerencia: tipo de cadena validada por expresiones regulares: https://github.com/Microsoft/TypeScript/issues/6579

(pregunta SO relacionada: https://stackoverflow.com/questions/3895478/does-javascript-have-a-method-like-range-to-generate-a-range-within-the-supp)

También sería genial si pudieras hacer algo como number in range eg if 1 in 1..2 o inclusive if 1 in 1..=2 rust lo maneja bien https://doc.rust-lang.org/reference/ expresiones / range-expr.html . Eso ahorraría mucho espacio

¿Python y rust ya no tienen una función de rango escrito?
¿Podemos simplemente reimplementar las soluciones existentes en otros idiomas en lugar de reinventar la rueda o hacerla más genérica con cadenas, flotadores, etc., mientras que el propósito principal y el caso de uso más grande son los rangos de números? Podemos agregar más más tarde cuando sea necesario

Me he encontrado con esto en más de una ocasión y vine aquí a buscar una forma de definir un tipo que es una fracción entre 0 ... 1; esto se está discutiendo en https://news.ycombinator.com/item?id= 24362658 # 24372935 porque no hay una palabra correcta en inglés para definir este tipo de valores. Si la escritura mecanografiada puede ayudar aquí, sería excelente, pero puede ser extremadamente difícil de aplicar a nivel de tipografía.

@andrewphillipo También vi que se hablaba de eso y aprendí sobre el término Intervalo de unidad de una de las respuestas en el intercambio de pila, que parece ser la forma correcta de referirse a ese rango específico en un contexto genérico.

Si los rangos como tipos se implementan alguna vez en mecanografiado, quizás UnitInterval podría definirse como un tipo global, para fomentar una nomenclatura común en torno a algo que usamos con frecuencia en programación.

Sí UnitInterval es el nombre correcto para el rango 0 ... 1, por lo que es correcto para el tipo. Todavía no es correcto para el nombre del número, por lo que sería excelente si estuviera disponible para describir nuestro código con mayor precisión utilizando un tipo de este tipo. ¿Cómo funciona esto bajo el capó en el sistema de tipos de Rust? ¿Es solo un guardia o?

Si CantorSpace no es demasiado exagerado, creo que sería una definición justa del "rango" de "todos" los números reales entre [0, 1] como lo entiende una computadora. La asignación de estos a valores en tiempo de compilación no se puede inferir por un límite superior e inferior de Math.floor o Math.ceil desde Math.ceil(0) === 0 , y Math.floor(1) === 1 que es desgraciado.

Si se usara el conjunto de todos los números (0, 1) en su lugar, funcionaría usando el ejemplo anterior, pero eso es algo malo para excluir los valores referenciados como porcentajes en el lenguaje cotidiano. Si es posible, sería bueno que el compilador incluyera los límites de alguna manera, tal vez mediante una estricta verificación === con los casos límite 0 y 1.

o ¿Quizás usar 1~~3 para enteros y 0.1~0.5 para flotantes?

~ ya está tomado por el operador NOT bit a bit unario.

Por qué tenemos que hablar de esos malditos flotadores, el problema que tenemos son los números enteros. 99,99% es un problema de números enteros 0,001% es un problema de flotación.
No hay una solución general fácilmente posible, así que vayamos al rango de números de 0..10 como se ve en la mayoría de los lenguajes de programación, como ya se implementó hace años.

Deja que el flotador hable

Tal vez podamos dividir la cuenta y ampliar esta propuesta para el caso int:
https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c

Y para los flotadores, actualmente estoy trabajando en una propuesta de diseño diferente basada en algunas ideas de este número (principalmente @AnyhowStep; básicamente la capacidad de tener un tipo de intervalo abierto).

Mantenlo simple con
x..y para números enteros.
0.1..2 daría un error
1..2.1 también daría un error
Y cualquier número no entero detectable.
Diablos, copia la lógica de implementación de
https://en.m.wikibooks.org/wiki/Ada_Programming/Types/range
O
https://kotlinlang.org/docs/reference/ranges.html
O
https://doc.rust-lang.org/reference/expressions/range-expr.html
y llámalo un día.

✅No es necesario reinventar la rueda
✅No es necesario tener en cuenta los flotadores
✅ Coherencia en todos los idiomas
✅ Código estable probado y copiable de fuentes existentes

Vale la pena el esfuerzo para resolver flotantes, ya que la verificación de tipo basada en "int" es trivial para números <1000 al codificar el valor de tipo como una unión de cada entero fijo 0..1000, por lo que se dice que "no es necesario" volver -inventar la rueda "es un poco irónico. ¿Por qué no usar lo que ya funciona para casos de uso simples?

Preferiría ver algo de espacio para permitir una definición de tipo que entienda cosas como números irracionales. La capacidad de detectar invariantes en tiempo de compilación cuando se trabaja con radianes sería un caso de uso interesante. Apoyar τ como un argumento para un límite de valor de rango es un buen objetivo para afirmar que esta característica funciona según lo previsto.

Tenga en cuenta que no hay integer explícitos en javascript. Todo es solo un number , que se almacena como flotante. Si se implementa el operador de tipo de rango, debe manejar cualquier valor que number pueda manejar.

@aMoniker dependiendo de la definición de explicit : BigInt

Incluso para bigint, hay casos de uso que las uniones no pueden resolver, como asegurarse de que un bigint se ajuste a un bigint firmado / no firmado de MySQL

¿Qué hay de crear un tipo de rango intrinsic para esta función?

lib.es5.d.ts (o lib.es2020.bigint.d.ts por incluir soporte para 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`)
 */
userland
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

Esto realmente necesita algunos casos de uso convincentes, porque la implementación de uniones hoy en día es completamente inadecuada para realizar uniones de este tamaño. Algo que sucedería si escribieras algo como esto ...

Lo siguiente no será correcto sin un tipo 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

Tener el siguiente tipo evitará este problema:

type TUInt = 0..4294967295;

Otro caso de uso para tener el tipo Int32 y Int64 específicamente es cuando las personas comienzan a anotar su código con él ... abrirá la puerta a una mejor interoperabilidad con otros lenguajes ... casi todos escritos estáticamente los lenguajes tienen un tipo entero: Java, C #, C, Rust, F #, Go ... etc.

Si quiero llamar a una biblioteca npm escrita en TypeScript desde C #, por ejemplo, hay bibliotecas que toman definiciones de TypeScript y me crean una interfaz en C #, pero el problema es que el tipo number es float que no se puede usar para indexar una matriz en C # sin ponerla en mayúscula ... etc.

Otros casos de uso: más fácil de transpilar entre idiomas, optimización del rendimiento ... etc.

Tenga en cuenta que un caso más en el que esto sería útil es la interacción con WebAssembly, que tiene tipos de números separados muy explícitos (supongo que se mencionó brevemente como caso de uso de C #, pero quería aclarar que se aplica mucho más que eso).

UPD: Nvm, veo que se mencionó en https://github.com/microsoft/TypeScript/issues/15480#issuecomment -365420315 que Github ha escondido "amablemente" como "elementos ocultos".

+1

¿Fue útil esta página
0 / 5 - 0 calificaciones