Typescript: Suggestion : Plage en tant que type de numéro

Créé le 30 avr. 2017  ·  106Commentaires  ·  Source: microsoft/TypeScript

Lors de la définition d'un type, on peut spécifier plusieurs nombres séparés par | .

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

Permet de spécifier des types de nombres sous forme de plages, au lieu de répertorier chaque nombre :

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

Peut-être utiliser .. pour les entiers et ... pour les flottants.

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

Commentaire le plus utile

Cette idée peut être étendue aux caractères, par exemple "b".."d" serait "b" | "c" | "d" . Il serait plus facile de spécifier des jeux de caractères.

Tous les 106 commentaires

Cette idée peut être étendue aux caractères, par exemple "b".."d" serait "b" | "c" | "d" . Il serait plus facile de spécifier des jeux de caractères.

Je pense que cela peut être étendu et utiliser une syntaxe et une sémantique comme les plages Haskell.

| Syntaxe | Désucrée |
|--------------------------|----------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------|
| type U = (e1..e3) | type U = \| e1 \| e1+1 \| e1+2 \| ...e3 \|
L'union est never si e1 > e3 |
| type U2 = (e1, e2..e3) | type U2 = \| e1 \| e1+i \| e1+2i \| ...e3 \| ,
où l'incrément, i , est e2-e1 .

Si l'incrément est positif ou nul, l'union se termine lorsque l'élément suivant sera supérieur à e3 ;
l'union est never si e1 > e3 .

Si l'incrément est négatif, l'union se termine lorsque l'élément suivant sera inférieur à e3 ;
l'union est never si e1 < e3 . |

@panuhorsmalahti Et si vous "bb".."dd" ?

@streamich

Peut-être utiliser .. pour les entiers et ... pour les flottants.

J'aime vraiment l'idée de générer des types intégraux comme celui-ci, mais je ne vois pas comment les valeurs à virgule flottante pourraient fonctionner.

@aluanhaddad Dites probabilité :

type TProbability = 0.0...1.0;

@streamich pour que ce type ait un nombre théoriquement infini d'habitants possibles ?

@aluanhaddad en fait, ce serait loin d'être infini en virgule flottante IEEE. Elle aurait 1 065 353 217 habitants selon mes calculs.

0.0...1.0 ? JS utilise IEEE double, c'est 53 bits de plage dynamique. Si cela devait être pris en charge, les gammes devraient être d'un type de première classe, ce qui serait irréalisable pour un syndicat.

@jcready en effet mais, comme le souligne @fatcerberus , le réaliser en tant que type d'union serait prohibitif.

Ce à quoi je voulais en venir, d'une manière détournée, c'est que cela introduirait une certaine notion de types discrets vs continus dans le langage.

le réaliser comme un type d'union serait prohibitif.

@aluanhaddad Oui, mais même spécifier un entier non signé en tant qu'union serait très coûteux :

type TUInt = 0..4294967295;

Cela nécessite vraiment des cas d'utilisation convaincants, car la mise en œuvre des syndicats aujourd'hui est totalement inadaptée à la réalisation de syndicats de cette taille. Quelque chose qui arriverait si vous écriviez quelque chose comme ça

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

serait l'instanciation du type d'union 0 | 1 | 2 | 3 | 5 | 6 | 7 | ... .

Peut-être que cela ne pourrait fonctionner que contre des littéraux numériques. Toutes les valeurs numériques non littérales devraient être explicitement affinées avec des comparaisons supérieures/inférieures à avant d'être considérées comme faisant partie de la plage. Les plages entières nécessiteraient également une vérification supplémentaire de Number.isInteger() . Cela devrait éliminer le besoin de générer des types d'union réels.

@RyanCavanaugh Types de soustraction ? ??

Types négatifs, type négation.

Tout sauf une chaîne :

type NotAString = !string;

N'importe quel nombre sauf zéro :

type NonZeroNumber = number & !0;

Les types de soustraction

Mon cas d'utilisation est le suivant : j'aimerais taper un paramètre sous la forme 0 ou un nombre positif (c'est un index de tableau).

@RoyTinker Je pense vraiment que ce serait cool mais je ne sais pas si ce cas d'utilisation aide l'argument.
Un tableau n'est qu'un objet et les index croissants ne sont qu'une convention.

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

donc vous devez finalement toujours effectuer le même contrôle

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

Ce serait très utile pour définir des types comme seconde, minute, heure, jour, mois, etc.

@Frikki, ces unités sont sur un intervalle suffisamment restreint pour qu'il soit pratique et extrêmement difficile de les écrire à la main.

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 Mais pas d'int non signé :

type UInt = 0..4294967295;

meh, que diriez-vous d'un type comme celui-ci :

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

Utilisation * opérateur

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

@streamich Doubler le nombre de bits ne correspond pas à une multiplication par deux, c'est plutôt une exponentation avec 2 comme exposant. Ce n'est toujours pas correct, car vous ne devriez pas augmenter la limite supérieure, mais les nombres encodables comptent. Dans l'ensemble, ce n'est pas une bonne stratégie de définition.

@streamich , quelques commentaires :

  • L'utilisation des termes char , word , etc. peut être source de confusion car les personnes d'autres langages pourraient ne pas réaliser la différence entre la définition statique et le comportement à l'exécution.
  • La syntaxe que vous proposez ne prend pas en compte la borne inférieure -- et si elle n'est pas nulle ?
  • Je me méfierais de la cooptation de l'opérateur d'exponentiation pour une utilisation dans un contexte ambiant/de type, car il a déjà été ajouté à ES2016.

complétons simplement le système de types et profitons du problème d'arrêt lorsque nous atteignons Ctrl + Shift + B

@aleksey-bykov vous vous souvenez sûrement de ce beau problème 😀

@aluanhaddad

ces unités [unités de temps] sont sur un intervalle suffisamment restreint pour qu'il soit pratique et extrêmement difficile de les écrire à la main.

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

Maintenant, faites-le en quelques millisecondes :wink:

Ce problème est-il mort ?

@Palid il est étiqueté avec [Needs Proposal] donc j'en doute.

Aussi amusante que la discussion ait été, la plupart d'entre nous n'ont pas réussi à fournir des cas d'utilisation convaincants dans le monde réel.

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

__CAS D'UTILISATION :__

  1. Vous pouvez définir des types int précis de sorte que le sous-ensemble de TypeScript puisse être compilé vers WebAssembly ou une autre cible.

  2. Un autre cas d'utilisation est UInt8Array , Int8Array , Uint16Array , etc., lorsque vous lisez ou écrivez des données à partir de ces TypeScript pourrait vérifier les erreurs.

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

uint8[0] = 0xFFFFFFFF; // TSError: Number too big!
  1. J'ai mentionné quelques cas d'utilisation dans mon OP.

  2. Si vous implémentez cela, la communauté TypeScript proposera des millions de cas d'utilisation.

J'ai un cas d'utilisation vraiment amusant, qui est peut-être plus proche de tout ce qui est proposé ici.
L'API prend un entier entre une plage (dans mon cas 5-30) et nous devons implémenter un SDK pour cela.
L'écriture manuelle en dessous est fastidieuse (bien que cela puisse être un peu automatisé) pour 25 valeurs, mais qu'en est-il des centaines ou des milliers ?
type X = 5 | 6 | 7 | 8 | 9 | 10 ... | 30

@Palid C'est le cas le meilleur et le plus simple que j'ai vu pour cette fonctionnalité.

Si une fourchette comme 5..30 était définie comme du sucre syntaxique pour 5 | 6 | 7 ... | 30 (ou fonctionne de manière identique), je parie que ce serait une victoire facile. Ici, il représenterait une plage discrète d'entiers.

Peut-être qu'une plage continue (par opposition à discrète) pourrait être notée en utilisant un nombre avec un point -- 5.0..30.0 .

J'envisageais en fait de regarder Typescript pour implémenter une protection de frappe pour
https://www.npmjs.com/package/memory-efficient-object

Avoir une plage de typage permettrait de détecter plus facilement un débordement potentiel de mémoire au moment de la vérification du type

On dirait que nous n'avons pas vraiment besoin d'une expansion syndicale, mais plutôt d'une définition de plage inclusive/exclusive qui compare simplement la plage

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

Cela serait très utile lors du contrôle des moteurs (en utilisant Johnny-Five, par exemple), où la vitesse varie de 0 à 255.

Autre cas d'utilisation : j'implémente un programme de dessin basé sur Canvas à l'aide de TypeScript et j'aimerais avoir des vérifications de type sur l'opacité (qui doit être un nombre compris entre 0,0 et 1,0).

Juste en pensant ... pour mettre cela en œuvre correctement, vous devriez vraiment tout mettre en œuvre :

  • prend en charge les fonctions de garde de type d'exécution
  • rétrécissement de type pour des conditions telles que x <= 10
  • prise en charge des plages de string | number (puisque x == 5 est true pour x === "5" ) en plus des plages de nombres uniquement
  • probablement même la prise en charge d'un nouveau type int (qui serait un sous-type de number ), et la prise en charge des gammes int -only et string | int . Tapez également le rétrécissement pour des expressions comme x|0

Excellente idée, mais ce serait beaucoup à couvrir!

Peut-être que nous n'avons pas besoin d'une protection de type à l'exécution. Au lieu de cela, nous pourrions faire en sorte que le temps de compilation soit entièrement protégé

Cela nécessiterait plutôt une prédiction de branche élaborée

Supposer

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

Mon cas d'utilisation :

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

Ce serait bien de pouvoir restreindre les clés dans l'option statusCode telle sorte qu'au moment de la compilation, il puisse être déterminé si un code d'état correspond à un code de réussite ou d'erreur :

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

OMI, cela devrait être limité aux flottants et aux entiers et ne devrait pas être implémenté comme une union, mais plutôt comme un nouveau type de plage. Ensuite, la vérification du type serait aussi simple que :

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

Nous pourrions peut-être prendre une feuille du livre de Ruby et utiliser .. pour les plages inclusives ( [start, stop] ) et ... pour les plages non inclusives ( [start, stop) ).

Un autre cas d'utilisation serait de taper vérifier vos enregistrements de base de données :

Un autre cas d'utilisation serait la vérification de type des valeurs Lat/Lon.

Cela pourrait être couvert par le mécanisme de validation général de #8665 (je ne sais pas pourquoi il a été fermé en tant que doublon) :

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;

Ou pris avec #4639, et en supposant que le type entier non signé est défini comme uint :

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

Mon cas d'utilisation est les coordonnées globales. Je veux taper la latitude et la longitude afin qu'elles ne tombent que dans les plages spécifiques (-90 à 90 et -180 à 180).

Edit: lat et long ont une plage négative

Mon cas d'utilisation est l'implémentation de tableaux de taille fixe où la taille est un paramètre.

Par exemple, je souhaite définir un type pour les tableaux de chaînes de longueur 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

Avec la version actuelle de TS, il est possible de définir quelque chose de très proche de la "spécification" ci-dessus. Le problème principal est l'accès des membres : par exemple, d[1] = 'b1' renvoie une erreur de type même si elle est correcte. Afin d'éviter l'erreur, la liste de tous les indices légaux doit être compilée à la main dans la définition de FixedSizeArray , ce qui est ennuyeux.

Si nous avions un opérateur range similaire à l'opérateur keyof , la définition de type suivante devrait résoudre le problème.

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

range(N) est un raccourci pour range(0,N) .

Étant donné les littéraux numériques (naturels) M, N avec M < N,

type r = range(M, N); 

est équivalent à

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

Ce qui pourrait être plus général, c'est si nous pouvions définir des lambdas de prédicat et les utiliser comme types. Exemple:

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

Un pro serait une faible complexité de syntaxe car les Lambdas sont déjà disponibles, tout ce dont nous avons besoin est la possibilité de les utiliser comme types, la vérification de type est de toute façon spécifique au dactylographe (en gardant à l'esprit la philosophie "superset to Javascript")
Un inconvénient est que la complexité du prédicat déterminera les performances de l'outillage pour fournir un retour d'information.

S'assurer qu'un nombre/caractère appartient à une progression arithmétique pourrait être un bon cas d'utilisation courant :

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

cela nous permettrait de faire des vérifications de type comme :
prédicat appartientÀMonAP = appartientÀAP(1,1, 10)

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

Cela peut également être étendu aux personnages.

@Kasahs Quelque chose de similaire a déjà été proposé dans #8665.

Jeter mon lot avec le cas d'utilisation « refléter une API ». Un point de terminaison REST pour lequel j'écris une fonction wrapper prend un entier dans la plage 1..1000 comme argument. Il génère une erreur si le nombre ne satisfait pas cette contrainte.

J'écris donc une proposition de plages numériques et j'ai rencontré ce problème que je ne sais pas comment traiter, alors je le jette pour examen.

Étant donné que le compilateur TypeScript est écrit en TypeScript, il est possible d'exploiter les propriétés des opérateurs numériques pour muter les types de plage.

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

C'est bien lors de l'affectation à une nouvelle variable, mais qu'en est-il de la mutation ?

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

Cette erreur serait techniquement correcte, mais en pratique, elle serait incroyablement ennuyeuse à gérer. Si nous voulons muter une variable retournée par une fonction avec un type de retour de plage, nous devrons toujours ajouter une assertion de type.

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.

Et si nous l'ignorons, nous obtenons l'anomalie suivante :

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.

Un correctif pour cela serait la possibilité de changer le type d'une variable après sa déclaration, donc l'exemple 2 changerait simplement le type de x en 2..12 ; mais mon premier réflexe est que cela introduirait trop de surcharge dans le compilateur et serait déroutant pour les utilisateurs.

Et qu'en est-il des fonctions définies par l'utilisateur et des génériques ?

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

Avez-vous des idées sur la façon de traiter ce qui précède ?

@JakeTunaley

qu'en est-il des mutations ?

Pourquoi la mutation devrait-elle être différente de toute autre affectation ?

Voici mon humble tentative de proposition. Cette proposition demande également que d'autres types soient ajoutés.

  • Infinity type

    • A seulement la valeur Infinity

  • -Infinity type

    • A seulement la valeur -Infinity

  • NaN type

    • A seulement la valeur NaN

  • double type

    • Toutes les valeurs number dans la plage [-Number.MAX_VALUE, Number.MAX_VALUE] ou, [-1.7976931348623157e+308, 1.7976931348623157e+308]

  • number est juste Infinity|-Infinity|NaN|double
  • int type

    • Un sous-type de double

    • Toutes les valeurs number x dans la plage [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] ou [-9007199254740991, 9007199254740991] et Math.floor(x) === x

    • Ainsi, 3 et 3.0 seraient des valeurs de type int

  • Type "littéral fini"

    • Les exemples sont 1 , 3.141 , 45

    • Peut être des sous-types de double ou int

  • Le type "GtEq" noté (>= x)

    • x est un littéral fini, Infinity , ou -Infinity

  • Le type "LtEq" noté (<= x)

    • x est un littéral fini, Infinity , ou -Infinity

  • Le type "Gt" noté (> x)

    • x est un littéral fini, Infinity , ou -Infinity

  • Le type "Lt" noté (< x)

    • x est un littéral fini, Infinity , ou -Infinity


type GtEq ; (>= x)

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

type GT ; (> x)

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

type LtEq ; (<= x)

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

Type Lt ; (< x)

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

Types de plage

Notez que même si nous pouvons écrire des choses comme (>= Infinity) , (> number) , etc.,
le type résultant n'est pas un type plage ; ce ne sont que des alias pour d'autres types.

Un type de plage est l'un des,

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

Nous autorisons une syntaxe comme (> number) et ses semblables pour une utilisation dans les génériques.


Types d'union GtEq/Gt

En prenant l'union de deux types GtEq/Gt, le type avec "plus" de valeurs est le résultat,

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

    • Si [finite-literal-A] >= [finite-literal-B] , alors le résultat est (>= [finite-literal-B])

    • Sinon, le résultat est (>= [finite-literal-A])

    • par exemple (>= 3) | (>= 5.5) = (>= 3) parce que (>= 3) est un super-type de (>= 5.5)

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

    • Si [finite-literal-A] == [finite-literal-B] , alors le résultat est (>= [finite-literal-A])

    • Si [finite-literal-A] > [finite-literal-B] , alors le résultat est (> [finite-literal-B])

    • Sinon, le résultat est (>= [finite-literal-A])

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

    • Si [finite-literal-A] >= [finite-literal-B] , alors le résultat est (> [finite-literal-B])

    • Sinon, le résultat est (> [finite-literal-A])

    • par exemple (> 3) | (> 5.5) = (> 3) parce que (> 3) est un super-type de (> 5.5)

Aussi,

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

    • par exemple (> 4|3) = (> 4) | (> 3) = (> 3)

    • par exemple (> number|3) = (> number) | (> 3) = number | (> 3) = number

Union types LtEq/Lt

En prenant l'union de deux types LtEq/Lt, le type avec "plus" de valeurs est le résultat,

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

    • Si [finite-literal-A] <= [finite-literal-B] , alors le résultat est (<= [finite-literal-B])

    • Sinon, le résultat est (<= [finite-literal-A])

    • par exemple (<= 3) | (<= 5.5) = (<= 5.5) parce que (<= 5.5) est un super-type de (<= 3)

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

    • Si [finite-literal-A] == [finite-literal-B] , alors le résultat est (<= [finite-literal-A])

    • Si [finite-literal-A] < [finite-literal-B] , alors le résultat est (< [finite-literal-B])

    • Sinon, le résultat est (<= [finite-literal-A])

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

    • Si [finite-literal-A] <= [finite-literal-B] , alors le résultat est (< [finite-literal-B])

    • Sinon, le résultat est (< [finite-literal-A])

    • par exemple (< 3) | (< 5.5) = (< 5.5) parce que (< 5.5) est un super-type de (< 3)

Aussi,

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

    • par exemple (< 4|3) = (< 4) | (< 3) = (< 4)

    • par exemple (< number|3) = (< number) | (< 3) = number | (< 3) = number


Types d'intersection GtEq/Gt

En prenant l'intersection de deux types GtEq/Gt, le type avec "moins" de valeurs est le résultat,

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

    • Si [finite-literal-A] >= [finite-literal-B] , alors le résultat est (>= [finite-literal-A])

    • Sinon, le résultat est (>= [finite-literal-B])

    • par exemple (>= 3) & (>= 5.5) = (>= 5.5) parce que (>= 5.5) est un sous-type de (>= 3)

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

    • Si [finite-literal-A] == [finite-literal-B] , alors le résultat est (> [finite-literal-B])

    • Si [finite-literal-A] > [finite-literal-B] , alors le résultat est (>= [finite-literal-A])

    • Sinon, le résultat est (> [finite-literal-B])

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

    • Si [finite-literal-A] >= [finite-literal-B] , alors le résultat est (> [finite-literal-A])

    • Sinon, le résultat est (> [finite-literal-B])

    • par exemple (> 3) & (> 5.5) = (> 5.5) parce que (> 5.5) est un sous-type de (> 3)

Types d'intersection LtEq/Lt

En prenant l'intersection de deux types LtEq/Lt, le type avec "moins" de valeurs est le résultat,

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

    • Si [finite-literal-A] <= [finite-literal-B] , alors le résultat est (<= [finite-literal-A])

    • Sinon, le résultat est (<= [finite-literal-B])

    • par exemple (<= 3) & (<= 5.5) = (<= 3) parce que (<= 3) est un sous-type de (<= 5.5)

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

    • Si [finite-literal-A] == [finite-literal-B] , alors le résultat est (< [finite-literal-B])

    • Si [finite-literal-A] < [finite-literal-B] , alors le résultat est (<= [finite-literal-A])

    • Sinon, le résultat est (< [finite-literal-B])

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

    • Si [finite-literal-A] <= [finite-literal-B] , alors le résultat est (< [finite-literal-A])

    • Sinon, le résultat est (< [finite-literal-B])

    • par exemple (< 3) & (< 5.5) = (< 3) parce que (< 3) est un sous-type de (< 5.5)


Cas d'utilisation

  • Pour s'assurer statiquement qu'un entier peut tenir dans un type de données MySQL UNSIGNED INT ,

    //TODO Propose numeric and range sum/subtraction/multiplication/division/mod/exponentiation types?
    function insertToDb (x : int & (>= 0) & (<= 4294967295)) {
      //Insert to database
    }
    
  • Pour s'assurer statiquement qu'une chaîne a une longueur donnée,

    function foo (s : string & { length : int & (>= 1) & (<= 255) }) {
      //Do something with this non-empty string that has up to 255 characters
    }
    
  • Pour s'assurer statiquement qu'un objet de type tableau a la length appropriée,

    function foo (arr : { length : int & (>= 0) }) {
      //Do something with the array-like object
    }
    
  • Pour s'assurer statiquement qu'on ne nous donne que des nombres finis,

    function foo (x : double) {
      //`x` is NOT NaN|Infinity|-Infinity
    }
    
  • Pour garantir statiquement l'existence d'indices de tableau ?

    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
    }
    

J'aimerais proposer des types d'addition/soustraction/multiplication/division/mod/exponentiation numériques et de plage, mais cela semble hors de portée avec ce problème.

[Éditer]
Vous pouvez renommer double et l'appeler float mais je pensais juste que double représentait plus précisément qu'il s'agissait d' float nombre à virgule flottante double précision.

[Éditer]

Modification de certains types en never .

Serait-il possible que le compilateur fasse l'analyse des flux ?

Supposons qu'il existe cette fonction

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 autre cas d'utilisation : j'ai un nombre entré dans une fonction qui représente un pourcentage. Je veux limiter les valeurs entre 0 et 1.

Je viens de lancer [...Array(256)].map((_,i) => i).join("|") pour faire ma définition de type la plus laide à ce jour

Les entiers non négatifs et les petits nombres sont possibles :

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

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

Par "petits nombres" tu veux dire sans utiliser l'étape 3 BigInt ?

@Mouvedia Un nombre qui rentre dans la limite de récursivité du compilateur

Peut-être utiliser .. pour les entiers et ... pour les flottants.

Je dirais que .. devrait signifier une plage inclusive et ... exclusif. Tout comme dans Ruby, par exemple. Voir http://rubylearning.com/satishtalim/ruby_ranges.html

Je ne suis pas fan d'utiliser une seule période pour différencier les gammes inclusives et exclusives

Puis-je intervenir ? Je pense que cette fonctionnalité considérée comme une seule amélioration pour la spécification de type n'est probablement pas la meilleure façon de procéder ici.

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

C'est ainsi que vous définissez une plage numérique dans de nombreuses plates-formes 4gl, en particulier celles à orientation matricielle.

Vous procédez ainsi, car une plage uniforme est conventionnellement l'une des meilleures approches de modélisation.

Voici donc une plage séquentielle :

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

Et si seulement les entiers :

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

// OR

type integers = number.integers<1:10>

Si nous voulons jouer avec la syntaxe :

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

Si cela ne convient pas aux tokenizers, ce n'est pas la bonne priorité de blocage sur laquelle se concentrer ici à mon avis. Je le signale en espérant qu'une solution ne nous limite pas pour passer à la suivante.

edit : Ce que je ne prends pas en compte, c'est l'aspect inclusif - et ici nous devons nous demander si nous faisons suffisamment de diligence raisonnable pour comprendre pourquoi ce n'était pas un problème lorsque les gens ont résolu tant de problèmes en s'appuyant sur des plages uniformes qui incluent implicitement chaque nombre sauf lorsque l'incrément ne correspond pas exactement à la fin de la plage.

Intéressant que vous évoquiez la possibilité de définir des tailles de "pas" dans un type de plage.

Au-delà des plages "à virgule flottante" (pas de taille de pas fixe) et "entier" (taille de pas de 1, à partir d'un entier), je n'ai jamais rencontré de cas d'utilisation réel pour des plages avec d'autres tailles de pas.

Donc, c'est intéressant d'entendre que c'est quelque chose d'ailleurs. Maintenant, je vais devoir me renseigner sur le 4gl car je n'en ai jamais entendu parler auparavant.


Pouvoir définir un intervalle semi-ouvert est utile pour les objets de type tableau. Quelque chose comme,

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

Si nous n'avions que des plages inclusives, nous aurions besoin de (<= LengthT - 1) mais c'est juste moins élégant

Puis-je intervenir ? Je pense que cette fonctionnalité considérée comme une seule amélioration pour la spécification de type n'est probablement pas la meilleure façon de procéder ici.

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

C'est ainsi que vous définissez une plage numérique dans de nombreuses plates-formes 4gl, en particulier celles à orientation matricielle.

Vous procédez ainsi, car une plage uniforme est conventionnellement l'une des meilleures approches de modélisation.

Voici donc une plage séquentielle :

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

Et si seulement les entiers :

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

// OR

type integers = number.integers<1:10>

Si nous voulons jouer avec la syntaxe :

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

Si cela ne convient pas aux tokenizers, ce n'est pas la bonne priorité de blocage sur laquelle se concentrer ici à mon avis. Je le signale en espérant qu'une solution ne nous limite pas pour passer à la suivante.

edit : Ce que je ne prends pas en compte, c'est l'aspect inclusif - et ici nous devons nous demander si nous faisons suffisamment de diligence raisonnable pour comprendre pourquoi ce n'était pas un problème lorsque les gens ont résolu tant de problèmes en s'appuyant sur des plages uniformes qui incluent implicitement chaque nombre sauf lorsque l'incrément ne correspond pas exactement à la fin de la plage.

Hmm ressemble à la façon dont c'est géré dans Haskell. Je pense que c'est bon. Les générateurs permettront également une évaluation paresseuse.

Ce n'est pas comme si vous ne pouviez pas avoir d'évaluation paresseuse avec une autre syntaxe =x

Intéressant que vous évoquiez la possibilité de définir des tailles de "pas" dans un type de plage.

Je pense à une plage comme début, fin et incrément. Donc, si l'incrément s'arrondit exactement à la fin, il l'inclut. Les indices et les longueurs des tableaux, en tant que fonctions de plage, peuvent être un tableau 0:1:9 comportant 10 pas d'index (longueur). Ainsi, la plage ici peut être commodément de integer & 0:9 pour un système de types qui peut plus facilement déduire de la combinaison de ces deux expressions.

4GL est vraiment un label générique, pour moi c'était surtout MatLab

Mon point était que n'avoir que des plages inclusives rendrait son utilisation dans les génériques plus difficile (à moins que vous n'implémentiez les opérations mathématiques au niveau du type hack-y).

Parce qu'au lieu de n'avoir que "length" comme paramètre de type, vous avez maintenant besoin de length et d'index max. Et l'index max doit être égal à la longueur-1.

Et, encore une fois, vous ne pouvez pas vraiment vérifier que c'est le cas à moins que vous n'implémentiez ces opérations mathématiques de niveau de type hack-y

@AnyhowStep Je pensais à la meilleure façon de formuler votre préoccupation - mais cela aide peut-être à clarifier d'abord :

Si nous mettons de côté l'inclusif ou non comme spécifiquement applicable à n'importe quel problème n-1 (comme ce scénario d'index/de comptage), en supposant qu'il a été résolu indépendamment d'une manière ou d'une autre (juste faire plaisir à la pensée), existe-t-il d'autres scénarios pour lesquels des plages numériques seraient nécessitent toujours moins de syntaxe déclarative et/ou non conventionnelle pour décrire correctement ?

Je veux en venir à ces 2 aspects distincts, vous avez besoin de plages numériques qui sont conventionnellement alignées sur les attentes de ce domaine et vous avez également besoin de prédicats pour les types d'index/compte... etc.

Nombres positifs uniquement.

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

Avec des gammes inclusives uniquement,

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

Nombres positifs, à l'exception de Infinity ,

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

Avec des gammes inclusives uniquement,

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

Aussi, sans rapport avec la discussion actuelle, mais voici une autre raison pour laquelle je veux vraiment des types de plages numériques.

Une grande partie de mon travail quotidien consiste simplement à bricoler un tas de systèmes logiciels différents ensemble. Et beaucoup de ces systèmes ont des exigences différentes pour les données qui ont la même signification.

par exemple, (systèmeA, valeur entre 1 et 255) (systèmeB, valeur entre 3 et 73), etc.
par exemple (systemC. longueur de chaîne 7-88), (systemD, longueur de chaîne 9-99), (systemE, longueur de chaîne 2-101), etc.

À l'heure actuelle, je dois soigneusement documenter toutes ces exigences distinctes et m'assurer que les données d'un système peuvent être correctement mappées à un autre système. Si cela ne correspond pas, je dois trouver des solutions de contournement.

Je ne suis qu'humain. Je fais des erreurs. Je ne réalise pas que les données ne peuvent parfois pas être mappées. Les vérifications de portée échouent.

Avec les types de plages numériques, je peux enfin simplement déclarer les plages attendues par chaque système et laisser le compilateur faire la vérification pour moi.


Par exemple, je viens d'avoir une situation où l'API que j'utilisais avait une limite de longueur de chaîne de 10 k pour toutes les valeurs de chaîne. Eh bien, je n'avais aucun moyen de dire à TypeScript de vérifier que toutes les chaînes allant à l'API ont une longueur de chaîne <= 10k.

J'ai eu une erreur d'exécution au lieu d'une belle erreur de compilation où TS pourrait aller,

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

@AnyhowStep J'espère que vous

Je pense honnêtement que les cas d'utilisation devraient toujours piloter les fonctionnalités, et je pense que les problèmes liés à l'index et à la longueur sont ceux que beaucoup d'entre nous manquent parfois vraiment. Donc, je veux simplement aborder ce problème juste sous l'étiquette correcte - est-ce une chose de type nombre ou une chose de type indexé ? Je ne connais pas exactement la réponse, mais j'hésite à penser que le résoudre en tant que chose de type nombre ne créera pas par inadvertance beaucoup plus de problèmes pour les utilisateurs de type nombre ne partageant pas la même compréhension de ces aspects.

Alors, où d'autre la résolution d'un tel problème aurait-elle un sens pour tout le monde, c'est tout ce que je me demande à ce stade, des pensées ?

J'ai affaire à une API qui passe des tableaux d'octets, j'aimerais donc définir un type d'octet :

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

Cela serait également utile lorsque vous travaillez avec Uint8Array .

Si vous êtes comme moi et que vous êtes impatient d'obtenir des types de plage que vous pouvez réellement utiliser pour le moment, voici un extrait de code des types de plage en action.

TL;DR,
Les types de plage peuvent fonctionner maintenant

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
}

Terrain de jeux

Il utilise le type CompileError<> d'ici,
https://github.com/microsoft/TypeScript/issues/23689#issuecomment -512114782

Le type AssertStringLengthLt<> est l'endroit où la magie opère

En utilisant l'addition au niveau du type, vous pouvez avoir, str512 + str512 et obtenir un str1024

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

En regardant la solution temporaire . Vous vous souvenez des gardes de type ? :

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

Jetez donc un œil au code suivant :

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

C'est juste une idée mais avec les gardes de type, il est possible d'utiliser des plages.

Le problème ici est que vous ne pouvez pas faire cela,

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
}

Les protections de type seules ne sont

De plus, votre exemple n'utilise même pas de gardes de type.


Mon exemple de jouet idiot ci-dessus vous permet d'attribuer un type de plage à un autre type de plage (via les paramètres de fonction), si ses limites sont à l'intérieur de l'autre.


De plus, les types de balises, les types nominaux et les objets de valeur utilisés sur les primitives sont généralement un signe que le système de types n'est pas assez expressif.

Je déteste mon exemple de jouet idiot qui utilise des types d'étiquettes car il est extrêmement peu ergonomique. Vous pouvez vous référer à ce long commentaire pour savoir comment il est préférable que les plages soient une primitive.

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

Vous pouvez y parvenir avec la version actuelle 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

J'ai suivi la convention d'avoir des index de début inclus et de fin exclusifs, mais vous pouvez l'ajuster à vos besoins.

Les indices de survol du code vs dans les commentaires.

Notez que les nombres dans les conseils de survol sont triés de manière aléatoire.

image

Malheureusement, cela ne fonctionne que jusqu'à environ 10 éléments :(

Edit : il semble que Enumerate ne puisse gérer que jusqu'à 15 récursions (0 - 14)

@Shinigami92
Avec cette approche, Enumerate gère jusqu'à 40.
Il s'agit toujours d'une limitation, mais ce n'est peut-être pas si sévère dans la pratique.

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

Et maintenant, cela s'est avéré être trié d'une manière ou d'une autre par magie ;).

Les cas d'utilisation typiques pour moi utilisent des plages comme [1, 255] , [1, 2048] , [1, 4096] , [20, 80] , etc. La création de grands types d'union peut faire paniquer/ralentir TS . Mais ces solutions fonctionnent définitivement pour des gammes "plus petites"

@AnyhowStep
Sachant que le nombre de récursivités est une limite, nous devrions trouver le moyen d'effectuer une division/multiplication de nombres par deux en une seule opération non récursive à l'intérieur de la définition de type pour atteindre ces plages.

Les performances sont toujours un problème. J'ai déjà dû sélectionner des types d'unions utiles dans de grandes applications pour cette raison - bien que cela puisse être un exercice intéressant, ce n'est certainement pas une solution.

Il n'y a toujours pas de solutions parfaites, je suppose?

J'espère que cela peut être fait comme suit de manière générale (mais difficile cependant).

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

(c'est-à-dire que l'instruction / fonction javascript normale peut être utilisée ici, sauf que les variables sont remplacées par le type)

À ma connaissance, langage moderne = logique normale + métalogique, où métalogique = vérification de code + génération de code. De nombreux travaux essaient simplement de fusionner la syntaxe métalogique d'une manière élégante.

Peut-être pouvons-nous en quelque sorte supposer que les types de gamme ne sont pas une sorte de sucre, mais un concept de base ?

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

Pour compléter cela, nous devons déclarer le comportement sur certains cas

  1. Range<T, T> === T par définition
  2. Range<5, 1> === NaN par définition
  3. Range<NaN, T> === Range<T, NaN> === NaN car une telle valeur ne peut pas exister
  4. Range<1 | 2, 5> === Range<2, 5> la possibilité d'un plus grand nombre en tant que premier paramètre de type restreint la plage pour être plus courte
  5. Range<1, 4 | 5> === Range<1, 4> la possibilité d'un nombre inférieur en tant que deuxième paramètre de type restreint la plage pour qu'elle soit plus courte
  6. 1.5 extends Range<1, 2> devrait être vrai par définition
  7. Range<Range<A, B>, C> === NaN extends Range<A, B> ? NaN : Range<B, C> suit de 5 et 3
  8. Range<A, Range<B, C>> === NaN extends Range<B, C> ? NaN : Range<A, B> suit de 6 et 3

Et un peu sur les plages à l'intérieur des plages :

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

Les sous-types de plage pourraient même être définis de manière générique, en tant que fonction de comparaison de niveau de type (a: T, b: T) => '<' | '=' | '>' .

... y a-t-il une proposition pour l'utilisation de fonctions JS régulières au niveau du type ?

Il n'y a pas de proposition en soi, mais j'ai déjà écrit un prototype rapide . Malheureusement, les performances de l'IDE et la désinfection des entrées suscitent de grandes inquiétudes.

Le problème de https://developer.mozilla.org/en-US/docs/Web/API/range aussi je manque la fonctionnalité où je peux définir séries de nombres comme <1, n+2> - étape 2 à partir d'une équation ou plus compliquée.

Cette proposition TC39 atteindra probablement l'étape 4 avant que cela ne soit mis en œuvre.
Le billet a 3 ans.

Suggestion : Type de chaîne validé par Regex : https://github.com/Microsoft/TypeScript/issues/6579

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

Ce serait aussi cool si vous pouviez faire quelque chose comme number in range par exemple if 1 in 1..2 ou inclus if 1 in 1..=2 rouille le gère bien https://doc.rust-lang.org/reference/ expressions/range-expr.html . ça ferait gagner beaucoup de place

Python et rouille n'ont-ils pas déjà une fonction de plage typée ?
Pouvons-nous simplement réimplémenter des solutions existantes dans d'autres langages au lieu de réinventer la roue ou de la rendre plus générique avec des chaînes, des flottants, etc. alors que l'objectif principal et le plus grand cas d'utilisation sont les plages de nombres. Nous pouvons en ajouter plus tard si nécessaire

J'ai rencontré cela plus d'une fois et je suis venu ici chercher un moyen de définir un type qui est une fraction entre 0...1 - ceci est en cours de discussion sur https://news.ycombinator.com/item?id= 24362658#24372935 car il n'y a pas de mot correct en anglais pour définir ces types de valeurs. Si le texte dactylographié peut aider ici, ce serait excellent, mais il peut être extrêmement difficile à appliquer au niveau du type.

@andrewphillipo J'ai vu que cela était également discuté et j'ai appris le terme intervalle d'unité à partir de l' une des réponses sur l'échange de pile, ce qui semble être la bonne façon de faire référence à cette plage spécifique dans un contexte générique.

Si des plages en tant que types sont jamais implémentées en tapuscrit, peut-être que UnitInterval pourrait être défini comme un type global, pour encourager une nomenclature commune autour de quelque chose que nous utilisons fréquemment en programmation.

Oui UnitInterval est le nom correct pour la plage 0...1 donc c'est correct pour le type ! Toujours pas correct pour la dénomination du numéro, ce serait donc excellent si cela était disponible pour décrire notre code plus précisément en utilisant un tel type - comment cela fonctionne-t-il sous le capot dans le système de type de Rust - est-ce juste un garde ou?

Si CantorSpace n'est pas trop exagéré, je pense que ce serait une définition juste de la "plage" de "tous" les nombres réels entre [0, 1] tel qu'il est compris par un ordinateur. L'attribution de ces valeurs à des valeurs au moment de la compilation ne peut pas être déduite par une limite inférieure et supérieure de Math.floor ou Math.ceil de javascript puisque Math.ceil(0) === 0 , et Math.floor(1) === 1 qui est malheureux.

Si l'ensemble de tous les nombres (0, 1) était utilisé à la place, cela fonctionnerait en utilisant l'exemple ci-dessus, mais c'est un peu mauvais d'exclure les valeurs référencées sous forme de pourcentages dans le langage courant. Si possible, faire en sorte que le compilateur inclue les limites d'une manière ou d'une autre serait bien, peut-être par le biais d'une vérification stricte de === rapport aux cas limites 0 et 1.

ou Peut-être utiliser 1~~3 pour les entiers et 0.1~0.5 pour les flottants ?

~ est déjà pris par l'opérateur unaire NOT au niveau du bit.

Pourquoi nous devons parler de ces fichus flotteurs, le problème que nous avons est avec les nombres entiers. 99,99% est un problème entier 0,001% est un problème flottant.
Aucune solution générale facilement possible, alors optons pour la plage de nombres de 0..10 comme on le voit dans la plupart des langages de programmation, comme déjà implémenté il y a des années.

Arrête le flotteur de parler

Peut-être pouvons-nous diviser le projet de loi et étendre cette proposition pour le cas int :
https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c

Et pour les flotteurs, je travaille actuellement sur une proposition de conception différente basée sur certaines idées de ce numéro (principalement @AnyhowStep ; essentiellement la possibilité d'avoir un Open-Interval-Type).

Restez simple avec
x..y pour les nombres entiers uniquement.
0.1..2 donnerait une erreur
1..2.1 donnerait également une erreur
Et tout nombre non entier détectable.
Heck copier la logique d'implémentation de
https://en.m.wikibooks.org/wiki/Ada_Programming/Types/range
Ou
https://kotlinlang.org/docs/reference/ranges.html
Ou
https://doc.rust-lang.org/reference/expressions/range-expr.html
et appelez-le un jour.

Pas besoin de réinventer la roue
Pas besoin de tenir compte des flotteurs
Cohérence dans toutes les langues
✅Code stable testé et copiable à partir de sources existantes

Cela vaut la peine de résoudre les flottants, car la vérification de type basée sur "int" est triviale pour les nombres < 1000 en codant en dur la valeur de type comme une union de chaque entier fixe 0..1000, donc en disant qu'il n'y a "pas besoin" de "re -inventer la roue" est un peu ironique. Pourquoi ne pas utiliser ce qui fonctionne déjà pour des cas d'utilisation simples ?

Je préférerais qu'il y ait de la place pour permettre une définition de type qui comprend des choses comme les nombres irrationnels. La capacité de détecter des invariants au moment de la compilation lorsque l'on travaille avec des radians serait un cas d'utilisation intéressant. La prise en charge de τ comme argument d'une limite de valeur de plage est une bonne cible pour affirmer que cette fonctionnalité fonctionne comme prévu imo.

Gardez à l'esprit qu'il n'y a pas de integer explicite en javascript. Tout n'est qu'un number , qui est stocké sous forme de flottant. Si l'opérateur de type plage est implémenté, il doit gérer toute valeur que number peut gérer.

@aMoniker selon la définition de explicit : BigInt

Même pour le bigint, il existe des cas d'utilisation que les syndicats ne peuvent pas résoudre, comme s'assurer qu'un bigint s'intègre dans un bigint signé/non signé MySQL

Que diriez-vous de créer un type de plage intrinsic pour cette fonctionnalité ?

lib.es5.d.ts (ou lib.es2020.bigint.d.ts pour inclure la prise en charge des 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`)
 */
pays utilisateur
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

Cela nécessite vraiment des cas d'utilisation convaincants, car la mise en œuvre des syndicats aujourd'hui est totalement inadaptée à la réalisation de syndicats de cette taille. Quelque chose qui arriverait si vous écriviez quelque chose comme ça...

Les éléments suivants ne seront pas corrects sans type 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

Avoir le type suivant évitera ce problème :

type TUInt = 0..4294967295;

Un autre cas d'utilisation pour le type Int32 et Int64 est le moment où les gens commencent à annoter leur code avec ... ouvrira la porte à une meilleure interopérabilité avec d'autres langages ... presque tous dactylographiés statiquement les langages ont un type entier : Java, C#, C, Rust, F#, Go...etc.

Si je veux appeler une bibliothèque npm écrite en TypeScript à partir de C# par exemple, il existe des bibliothèques qui prennent des définitions TypeScript et créent une interface pour moi en C# mais le problème est que le type number est float qui ne peut pas être utilisé pour indexer un tableau en C# sans le mettre en casse... etc.

Autres cas d'utilisation : plus facile à transpiler entre langages, optimisation des performances...etc

Notez qu'un autre cas où cela serait utile est l'interaction avec WebAssembly, qui a des types de nombres séparés très explicites (je suppose qu'il a été brièvement mentionné comme cas d'utilisation C#, mais je voulais préciser qu'il s'applique beaucoup plus large que cela).

UPD: Nvm, je vois qu'il a été mentionné dans https://github.com/microsoft/TypeScript/issues/15480#issuecomment -365420315 que Github a "utilement" caché comme "éléments cachés".

+1

Cette page vous a été utile?
0 / 5 - 0 notes