microsoft / microsoft/TypeScript

Tag types

Ouverte
#4,895 57 commentaires 56 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Discussion
Langage dominant
Go
Étoiles
111k
Forks
14.3k
Merge moyen
2 j 4 h
PR mergées (30 j)
132

Description

Problem
  • There is no straight way to encode a predicate (some checking) about a value in its type.
Details

There are situations when a value has to pass some sort of check/validation prior to being used. For example: a min/max functions can only operate on a non-empty array so there must be a check if a given array has any elements. If we pass a plain array that might as well be empty, then we need to account for such case inside the min/max functions, by doing one of the following:

  • crashing
  • returning undefined
  • returning a given default value

This way the calling side has to deal with the consequences of min/max being called yet not being able to deliver.

function min<a>(values: a[], isGreater: (one: a, another: a) => boolean) : a {
   if (values.length < 1) { throw Error('Array is empty.'); }
   // ...
}
var found;
try {
   found = min([]);
} catch (e) {
   found = undefined;
}
Solution

A better idea is to leverage the type system to rule out a possibility of the min function being called with an empty array. In order to do so we might consider so called tag types.

A tag type is a qualifier type that indicates that some predicate about a value it is associated with holds true.


const enum AsNonEmpty {} // a tag type that encodes that a check for being non-empty was passed

function min<a>(values: a[] & AsNonEmpty) : a {
   // only values that are tagged with AsNonEmpty can be passed to this function
   // the type system is responsible for enforcing this constraint
   // a guarantee by the type system that only non-empty array will be passed makes
   // implementation of this function simpler because only one main case needs be considered
   // leaving empty-case situations outside of this function
}

min([]); // <-- compile error, tag is missing, the argument is not known to be non-empty
min([1, 2]); // <-- compile error, tag is missing again, the argument is not know to be non-empty

it's up to the developer in what circumstances an array gets its AsNonEmpty tag, which can be something like:

// guarntee is given by the server, so we always trust the data that comes from it
interface GuaranteedByServer {
    values: number[] & AsNonEmpty;
}

Also tags can be assigned at runtime:


function asNonEmpty(values: a[]) : (a[] & AsNonEmpty) | void {
    return values.length > 0 ? <a[] & AsNonEmpty> : undefined;
}

function isVoid<a>(value: a | void) : value is void {
  return value == null;
}

var values = asNonEmpty(Math.random() > 0.5 ? [1, 2, 3] : []);
if (isVoid(values)) {
   // there are no  elements in the array, so we can't call min
   var niceTry = min(values); // <-- compile error; 
} else {
    var found = min(values); // <-- ok, tag is there, safe to call
}

As was shown in the current version (1.6) an empty const enum type can be used as a marker type (AsNonEmpty in the above example), because

  • enums might not have any members and yet be different from the empty type
  • enums are branded (not assignable to one another)

However enums have their limitations:

  • enum is assignable by numbers
  • enum cannot hold a type parameter
  • enum cannot have members

A few more examples of what tag type can encode:

  • string & AsTrimmed & AsLowerCased & AsAtLeast3CharLong
  • number & AsNonNegative & AsEven
  • date & AsInWinter & AsFirstDayOfMonth

Custom types can also be augmented with tags. This is especially useful when the types are defined outside of the project and developers can't alter them.

  • User & AsHavingClearance

ALSO NOTE: In a way tag types are similar to boolean properties (flags), BUT they get type-erased and carry no rutime overhead whatsoever being a good example of a zero-cost abstraction.

UPDATED:

Also tag types can be used as units of measure in a way:

  • string & AsEmail, string & AsFirstName:

var email = <string & AsEmail> 'aleksey.bykov@yahoo.com';
var firstName = <string & AsFirstName> 'Aleksey';
firstName = email; // <-- compile error
  • number & In<Mhz>, number & In<Px>:

var freq = <number & In<Mhz>> 12000;
var width =   <number & In<Px>> 768;
freq = width; // <-- compile error

function divide<a, b>(left: number & In<a>, right: number & In<b> & AsNonZero) : number & In<Ratio<a, b>> {
     return <number & In<Ratio<a, b>>> left  / right;
}

var ratio = divide(freq, width); // <-- number & In<Ratio<Mhz, Px>>

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Le texte de l’issue constitue le point de départ disponible ; commencez par étudier le comportement de TypeScript concernant enum, les types d’intersection, les génériques et l’effacement des types qui y est décrit. Comparez les exemples proposés de tags et d’unités de mesure avec le comportement actuel du système de types. Le travail sera considéré comme terminé lorsqu’une conception arrêtée et le comportement correspondant du compilateur pour les types de prédicat branded seront définis, avec notamment des règles claires pour leur création et leur assignabilité.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
typescript
Domaine
compilers
Type d'issue
Fonctionnalité
Difficulté
5/5
Temps estimé
Plus d'une semaine
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
25/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.