Typescript: Comparaison avec le système de type de flux Facebook

Créé le 25 nov. 2014  ·  31Commentaires  ·  Source: microsoft/TypeScript

Avertissement: Ce problème n'a pas pour but de prouver que le flux est meilleur ou pire que TypeScript, je ne veux pas critiquer les travaux incroyables des deux équipes, mais énumérer les différences dans le système de type Flow et TypeScript et essayer d'évaluer quelle fonctionnalité pourrait améliorer TypeScript.

Je ne parlerai pas non plus des fonctionnalités manquantes dans Flow car le but est comme indiqué d'améliorer TypeScript.
Enfin, cette rubrique concerne uniquement le système de types et non les fonctionnalités es6 / es7 prises en charge.

mixed et any

À partir du document de flux:

  • mixte: le "supertype" de tous les types. Tout type peut couler dans un mix.
  • any: le type "dynamique". Tout type peut couler dans n'importe quel, et vice-versa

En gros, cela signifie qu'avec flow any est l'équivalent de TypeScript any et mixed est l'équivalent de TypeScript {} .

Le type Object avec flux

De flow doc:

Utilisez mixed pour annoter un emplacement qui peut prendre n'importe quoi, mais n'utilisez pas Object à la place! Il est déroutant de tout voir comme un objet, et si par hasard vous voulez dire "n'importe quel objet", il existe une meilleure façon de le spécifier, tout comme il existe un moyen de spécifier "n'importe quelle fonction".

Avec TypeScript Object est l'équivalent de {} et accepte n'importe quel type, avec Flow Object est l'équivalent de {} mais est différent de mixed , il n'accepte que Object (et pas les autres types primitifs comme string , number , boolean ou function ).

function logObjectKeys(object: Object): void {
  Object.keys(object).forEach(function (key) {
    console.log(key);
  });
}
logObjectKeys({ foo: 'bar' }); // valid with TypeScript and Flow
logObjectKeys(3); // valid with TypeScript, Error with flow

Dans cet exemple, le paramètre de logObjectKeys est étiqueté avec le type Object , pour TypeScript qui est l'équivalent de {} et donc il acceptera n'importe quel type, comme un number dans le cas du deuxième appel logObjectKeys(3) .
Avec Flow, les autres types primitifs ne sont pas compatibles avec Object et donc le vérificateur de type signalera une erreur avec le deuxième appel logObjectKeys(3) : _number est incompatible avec Object_.

Le type n'est pas nul

De flow doc:

En JavaScript, null convertit implicitement en tous les types primitifs; c'est aussi un habitant valide de tout type d'objet.
En revanche, Flow considère null comme une valeur distincte qui ne fait partie d'aucun autre type.

voir la section Flow doc

Étant donné que le document de flux est assez complet, je ne décrirai pas cette fonctionnalité en détail, gardez simplement à l'esprit que cela oblige le développeur à avoir toutes les variables à initialiser ou à marquer comme nullable, exemples:

var test: string; // error undefined is not compatible with `string`
var test: ?string;
function getLength() {
  return test.length // error Property length cannot be initialized possibly null or undefined value
}

Cependant, comme pour la fonction de protection de type TypeScript, le flux comprend la vérification non nulle:

var test: ?string;
function getLength() {
  if (test == null) {
    return 0;
  } else {
    return test.length; // no error
  }
}

function getLength2() {
  if (test == null) {
    test = '';
  }
  return test.length; // no error
}

Type d'intersection

voir la section Flow doc
voir le numéro 1256 de Correspondin TypeScript

Comme le flux TypeScript prend en charge les types d'union, il prend également en charge une nouvelle façon de combiner les types: les types d'intersection.
Avec object, les types d'intersection sont comme déclarer un mixins:

type A = { foo: string; };
type B = { bar : string; };
type AB = A & B;

AB a pour type { foo: string; bar : string;} ;

Pour les fonctions, cela équivaut à déclarer une surcharge:

type A = () => void & (t: string) => void
var func : A;

est équivalent à :

interface A {
  (): void;
  (t: string): void;
}
var func: A

Capture de résolution générique

Prenons l'exemple de TypeScript suivant:

declare function promisify<A,B>(func: (a: A) => B):   (a: A) => Promise<B>;
declare function identity<A>(a: A):  A;

var promisifiedIdentity = promisify(identity);

Avec TypeScript promisifiedIdentity aura pour type:

(a: {}) => Promise<{}>`.

Avec flow promisifiedIdentity aura pour type:

<A>(a: A) => Promise<A>

Inférence de type

Flow en général essaie de déduire plus de type que TypeScript.

Inférence de paramètres

Jetons un œil à cet exemple:

function logLength(obj) {
  console.log(obj.length);
}
logLength({length: 'hello'});
logLength([]);
logLength("hey");
logLength(3);

Avec TypeScript, aucune erreur n'est signalée, avec flow le dernier appel de logLength entraînera une erreur car number n'a pas length propriété

Changements de type déduits avec l'utilisation

Avec flow, sauf si vous tapez expressément votre variable, le type de cette variable changera avec l'utilisation de cette variable:

var x = "5"; // x is inferred as string
console.log(x.length); // ok x is a string and so has a length property
x = 5; // Inferred type is updated to `number`
x *= 5; // valid since x is now a number

Dans cet exemple, x a initialement le type string , mais lorsqu'il est affecté à un nombre, le type a été changé en number .
Avec dactylographié, l'affectation x = 5 entraînerait une erreur puisque x été précédemment assigné à string et son type ne peut pas changer.

Inférence des types d'Union

Une autre différence est que Flow propage l'inférence de type vers l'arrière pour élargir le type inféré en une union de type. Cet exemple provient de facebook / flow # 67 (commentaire)

class A { x: string; }
class B extends A { y: number; }
class C extends A { z: number; }

function foo() {
    var a = new B();
    if (true) a = new C(); // TypeScript reports an error, because a's type is already too narrow
    a.x; // Flow reports no error, because a's type is correctly inferred to be B | C
}

("correctement" provient du message d'origine.)
Puisque le flux a détecté que la variable a pouvait avoir le type B ou le type C fonction d'une instruction conditionnelle, il est maintenant inféré à B | C , et donc L'instruction a.x n'entraîne pas d'erreur puisque les deux types ont une propriété x , si nous avions essayé d'accéder à la propriété z et l'erreur aurait été générée.

Cela signifie que ce qui suit sera également compilé.

var x = "5"; // x is inferred as string
if ( true) { x = 5; } // Inferred type is updated to string | number
x.toString(); // Compiles
x += 5; // Compiles. Addition is defined for both string and number after all, although the result is very different

Éditer

  • Mise à jour des sections mixed et any , puisque mixed est l'équivalent de {} il n'y a pas besoin d'exemple.
  • Section ajoutée pour le type Object .
  • Ajout d'une section sur l'inférence de type

_N'hésitez pas à avertir si j'ai oublié quelque chose, je vais essayer de mettre à jour le problème._

Question

Commentaire le plus utile

Personnellement, la capture générique et la non-nullabilité sont des cibles de valeur _high_ de Flow. Je vais lire l'autre fil, mais je voulais aussi jeter mon 2c ici.

J'ai parfois le sentiment que l'avantage d'ajouter la non-nullabilité vaut presque n'importe quel coût. Il s'agit d'une condition d'erreur de probabilité si élevée et, bien que la possibilité de nullité par défaut affaiblisse la valeur intégrée pour le moment, TypeScript n'a même pas la capacité de discuter de la nullabilité en supposant simplement que c'est le cas partout.

Je voudrais annoter chaque variable que je pourrais trouver comme non nullable en un clin d'œil.

Tous les 31 commentaires

C'est intéressant et c'est un bon point de départ pour plus de discussion. Cela vous dérange-t-il si j'apporte des modifications de révision à l'article original pour plus de clarté?

Choses inattendues dans Flow (mettra à jour ce commentaire au fur et à mesure que j'enquêterai)

Inférence de type d'argument de fonction impaire:

/** Inference of argument typing doesn't seem
    to continue structurally? **/
function fn1(x) { return x * 4; }
fn1('hi'); // Error, expected
fn1(42); // OK

function fn2(x) { return x.length * 4; }
fn2('hi'); // OK
fn2({length: 3}); // OK
fn2({length: 'foo'}); // No error (??)
fn2(42); // Causes error to be reported at function definition, not call (??)

Aucune inférence de type à partir de littéraux d'objet:

var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { }

C'est intéressant et c'est un bon point de départ pour plus de discussion. Cela vous dérange-t-il si j'apporte des modifications de révision à l'article original pour plus de clarté?

Sentez-vous libre Comme je l'ai dit, le but est d'essayer d'investir un système de type flux pour voir si certaines fonctionnalités pourraient s'intégrer dans TypeScript one.

@RyanCavanaugh Je suppose que le dernier exemple:

var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { }

Est-ce qu'un bogue est lié à leur algorithme de vérification nulle, je vais le signaler.

Est

type A = () => void & (t: string) => void
var func : A;

Équivalent à

Declare A : () => void | (t: string) => void
var func : A;

Ou est-ce possible?

@ Davidhanson90 pas vraiment:

declare var func: ((t: number) => void) | ((t: string) => void)

func(3); //error
func('hello'); //error

dans cet exemple, le flux ne peut pas savoir quel type dans le type d'union func est donc il signale une erreur dans les deux cas

declare var func: ((t: number) => void) & ((t: string) => void)

func(3); //no error
func('hello'); //no error

func a les deux types donc les deux appels sont valides.

Y a-t-il une différence observable entre {} dans TypeScript et mixed dans Flow?

@RyanCavanaugh Je ne sais pas vraiment après réflexion, je pense que c'est à peu près la même chose encore d'y penser.

mixed n'a pas de propriétés, pas même les propriétés héritées d'Object.prototype que {} a (# 1108) C'est faux.

Une autre différence est que Flow propage l'inférence de type vers l'arrière pour élargir le type inféré en une union de type. Cet exemple provient de https://github.com/facebook/flow/issues/67#issuecomment -64221511

class A { x: string; }
class B extends A { y: number; }
class C extends A { z: number; }

function foo() {
    var a = new B();
    if (true) a = new C(); // TypeScript reports an error, because a's type is already too narrow
    a.x; // Flow reports no error, because a's type is correctly inferred to be B | C
}

("correctement" provient du message d'origine.)

Cela signifie que ce qui suit sera également compilé.

var x = "5"; // x is inferred as string
if ( true) { x = 5; } // Inferred type is updated to string | number
x.toString(); // Compiles
x += 5; // Compiles. Addition is defined for both string and number after all, although the result is very different

Edit: Testé le deuxième extrait de code et il compile vraiment.
Edit 2: Comme indiqué par @fdecampredon ci-dessous, le if (true) { } autour de la deuxième affectation est nécessaire pour que Flow infère le type comme string | number . Sans le if (true) il est déduit comme number place.

Aimez-vous ce comportement? Nous avons choisi cette voie lorsque nous avons discuté des types d'union et la valeur est douteuse. Ce n'est pas parce que le système de types a maintenant la possibilité de modéliser des types avec plusieurs états possibles qu'il est souhaitable de les utiliser partout. Apparemment, vous avez choisi d'utiliser un langage avec un vérificateur de type statique parce que vous désirez des erreurs de compilation lorsque vous faites des erreurs, pas seulement parce que vous aimez écrire des annotations de type;) C'est-à-dire que la plupart des langages donnent une erreur dans un exemple comme celui-ci (en particulier le second) non par manque de moyen de modéliser l'espace de type mais parce qu'ils croient en fait qu'il s'agit d'une erreur de codage (pour des raisons similaires, beaucoup évitent de prendre en charge de nombreuses opérations de transtypage / conversion implicites).

Par la même logique, je m'attendrais à ce comportement:

declare function foo<T>(x:T, y: T): T;
var r = foo(1, "a"); // no problem, T is just string|number

mais je ne veux vraiment pas de ce comportement.

@danquirk Je suis d'accord avec vous pour dire que déduire automatiquement le type d'union au lieu de signaler une erreur n'est pas un comportement que j'aime.
Mais je pense que cela vient de la philosophie du flux, plus qu'un vrai langage, l'équipe de flux essaie de créer simplement un vérificateur de type, leur objectif ultime est de pouvoir rendre du code `` plus sûr '' sans aucune annotation de type. Cela conduit à être moins strict.

La rigueur exacte est même discutable étant donné les effets de ce type de comportement. Souvent, il s'agit simplement de reporter une erreur (ou d'en cacher une entièrement). Nos anciennes règles d'inférence de type pour les arguments de type reflétaient en grande partie une philosophie similaire. En cas de doute, nous avons déduit {} pour un paramètre de type plutôt que d'en faire une erreur. Cela signifiait que vous pouviez faire des choses loufoques et toujours faire un ensemble minimal de comportements en toute sécurité sur le résultat (à savoir des choses comme toString ). La raison en est que certaines personnes font des choses loufoques dans JS et que nous devrions essayer de permettre ce que nous pouvons. Mais en pratique, la majorité des inférences à {} n'étaient en fait que des erreurs, et vous obligeant à attendre la première fois que vous avez pointé une variable de type T pour réaliser que c'était {} (ou de même un type d'union inattendu) et puis tracer en arrière était au mieux ennuyeux. Si vous ne l'avez jamais pointé (ou n'avez jamais renvoyé quelque chose de type T), vous n'avez pas remarqué l'erreur du tout jusqu'à l'exécution lorsque quelque chose a explosé (ou pire, des données corrompues). De même:

declare function foo(arg: number);
var x = "5";
x = 5;
foo(x); // error

Quelle est l'erreur ici? Est-il vraiment en train x passer foo ? Ou était-il en train de réaffecter x une valeur d'un type complètement différent de celui avec lequel il avait été initialisé? À quelle fréquence les gens font-ils vraiment intentionnellement ce genre de réinitialisation plutôt que de piétiner accidentellement quelque chose? Dans tous les cas, en déduisant un type d'union pour x pouvez-vous vraiment dire que le système de types était globalement moins strict s'il entraînait toujours une (pire) erreur? Ce type d'inférence n'est moins strict que si vous ne faites jamais rien de particulièrement significatif avec le type résultant, ce qui est généralement assez rare.

On peut soutenir que le fait de laisser null et undefined assignables à n'importe quel type masque les erreurs de la même manière, la plupart du temps une variable tapée avec un certain type et cachant une null conduira à un erreur lors de l'exécution.

Une partie non négligeable du marketing de Flow est basée sur le fait que leur vérificateur de type a plus de sens pour le code dans les endroits où TS inférerait any . Sa philosophie est que vous ne devriez pas avoir besoin d'ajouter des annotations pour que le compilateur infère des types. C'est pourquoi leur cadran d'inférence est tourné vers un paramètre beaucoup plus permissif que celui de TypeScript.

Cela revient à savoir si quelqu'un s'attend à ce que var x = new B(); x = new C(); (où B et C dérivent tous deux de A) compile ou non, et si c'est le cas, comment doit-il être déduit?

  1. Ne devrait pas compiler.
  2. Doit compiler et être inféré comme le type de base le plus dérivé commun aux hiérarchies de types de B et C - A. Pour l'exemple de nombre et de chaîne, ce serait {}
  3. Doit compiler et être inféré comme B | C .

TS fait actuellement (1) et Flow fait (3). Je préfère (1) et (2) beaucoup plus que (3).

Je voulais ajouter des exemples @Arnavion au numéro original, mais après avoir joué un peu, j'ai réalisé que les choses étaient plus étranges que ce que nous comprenions.
Dans cet exemple:

var x = "5"; // x is inferred as string
x = 5; // x is infered as number now
x.toString(); // Compiles, since number has a toString method
x += 5; // Compiles since x is a number
console.log(x.length) // error x is a number

Maintenant :

var x = '';
if (true) {
  x = 5;
}

après cet exemple, x est string | number
Et si je fais:

1. var x = ''; 
2. if (true) {
3.  x = 5;
4. }
5. x*=5;

J'ai eu une erreur à la ligne 1 disant: myFile.js line 1 string this type is incompatible with myFile.js line 5 number

J'ai encore besoin de comprendre la logique ici ...

Il y a aussi un point intéressant sur le flow que j'ai oublié:

function test(t: Object) { }

test('string'); //error

Fondamentalement, «Object» n'est pas compatible avec les autres types primitifs, je pense que l'un d'eux a du sens.

La «capture de résolution générique» est définitivement une fonctionnalité incontournable pour TS!

@fdecampredon Oui, vous avez raison. Avec var x = "5"; x = 5; x, le type inféré est mis à jour en number . En ajoutant le if (true) { } autour de la deuxième affectation, le vérificateur de type est amené à supposer que l'une ou l'autre des affectations est valide, c'est pourquoi le type inféré est mis à jour à la place en number | string .

L'erreur que vous obtenez myFile.js line 1 string this type is incompatible with myFile.js line 5 number est correcte, puisque number | string ne prend pas en charge l'opérateur * (les seules opérations autorisées sur un type union sont l'intersection de toutes les opérations sur tous les types de l'Union). Pour vérifier cela, vous pouvez le changer en x += 5 et vous verrez qu'il se compile.

J'ai mis à jour l'exemple dans mon commentaire pour avoir le if (true)

La «capture de résolution générique» est définitivement une fonctionnalité incontournable pour TS!

+1

@Arnavion , {} à B | C . La déduction de B | C élargit l'ensemble des programmes qui vérifient les types sans compromettre l'exactitude, ce qui est une propriété généralement souhaitable des systèmes de types.

L'exemple

declare function foo<T>(x:T, y: T): T;
var r = foo(1, "a"); // no problem, T is just string|number

vérifie déjà les types sous le compilateur actuel, sauf que T est inféré comme étant {} plutôt que string | number . Cela ne compromet pas l'exactitude, mais en gros c'est moins utile.

Inférer number | string au lieu de {} ne me semble pas problématique. Dans ce cas particulier, cela n'élargit pas l'ensemble des programmes valides, mais si les types partagent la structure, le système de types s'en rend compte et rend quelques méthodes et / ou propriétés supplémentaires valides ne semble être qu'une amélioration.

La déduction de B | C élargit l'ensemble des programmes qui vérifient le type sans compromettre l'exactitude

Je pense qu'autoriser l'opération + sur quelque chose qui peut être une chaîne ou un nombre compromet l'exactitude, car les opérations ne sont pas du tout similaires. Ce n'est pas comme la situation où l'opération appartient à une classe de base commune (mon option 2) - dans ce cas, vous pouvez vous attendre à une certaine similitude.

L'opérateur + ne serait pas appelable, car il aurait deux surcharges incompatibles - l'une où les deux arguments sont des nombres et l'autre où les deux sont des chaînes. Depuis B | C est plus étroit que la chaîne et le nombre, il ne serait pas autorisé comme argument dans l'une ou l'autre des surcharges.

Sauf que les fonctions sont bivariantes par rapport à leurs arguments, donc cela pourrait être un problème?

Je pensais que puisque var foo: string; console.log(foo + 5); console.log(foo + document); compile que l'opérateur string + autorisait tout sur le côté droit, donc string | number aurait + <number> comme opération valide. Mais tu as raison:

error TS2365: Operator '+' cannot be applied to types 'string | number' and 'number'.

De nombreux commentaires ont porté sur l'élargissement automatique des types dans Flow. Dans les deux cas, vous pouvez avoir le comportement souhaité en ajoutant une annotation. Dans TS, vous élargiriez explicitement à la déclaration: var x: number|string = 5; et dans Flow vous restreindriez à la déclaration: var x: number = 5; . Je pense que le cas qui ne nécessite pas de déclaration de type devrait être celui que les gens utilisent le plus souvent. Dans mes projets, je m'attendrais var x = 5; x = 'five'; ce que

Quant aux fonctionnalités Flow que je pense être les plus précieuses?

  1. Types non nuls
    Je pense que celui-ci a un très fort potentiel de réduction des bugs. Pour la compatibilité avec les définitions TS existantes, je l'imagine plus comme un modificateur non nul string! plutôt que comme le modificateur nullable de Flow ?string . Je vois trois problèmes avec ceci:
    Comment gérer l'initialisation des membres de la classe? _ (ils doivent probablement être affectés dans le ctor et s'ils peuvent échapper au ctor avant l'affectation, ils sont considérés comme nullables) _
    Comment gérer undefined ? _ (Flow contourne ce problème) _
    Peut-il fonctionner sans beaucoup de déclarations de types explicites?
  2. Différence entre mixed et Object .
    Parce que contrairement aux types primitifs C # ne peuvent pas être utilisés partout où un objet le peut. Essayez Object.keys(3) dans votre navigateur et vous obtiendrez une erreur. Mais ce n'est pas critique car je pense que les cas extrêmes sont peu nombreux.
  3. Capture de résolution générique
    L'exemple a du sens. Mais je ne peux pas dire que j'écris beaucoup de code qui en bénéficierait. Peut-être que cela aidera avec les bibliothèques fonctionnelles telles que Underscore?

Sur l'inférence de type d'union automatique: je suppose que "l'inférence de type" est limitée à la déclaration de type. Un mécanisme qui déduit implicitement une déclaration de type omis. Comme := dans Go. Je ne suis pas un théoricien de type, mais pour autant que je sache, l'inférence de type est une passe de compilateur qui ajoute une annotation de type explicite à chaque déclaration de variable implicite (ou argument de fonction), déduite du type de l'expression à partir de laquelle elle est attribuée. Autant que je sache, c'est ainsi que cela fonctionne pour ... eh bien ... tout autre mécanisme d'inférence de type là-bas. C #, Haskell, Go, ils fonctionnent tous de cette façon. Ou non?

Je comprends l'argument de laisser le JS réel utiliser la sémantique TS, mais c'est peut-être un bon point pour suivre d'autres langages à la place. Les types sont la seule différence qui définit entre JS et TS, après tout.

J'aime beaucoup d'idées Flux, mais celle-ci, eh bien, si c'est vraiment fait de cette façon ... c'est juste bizarre.

Les types non nuls semblent être une fonctionnalité obligatoire pour un système de type moderne. Serait-il facile d'ajouter à ts?

Si vous voulez une lecture légère sur la complexité de l'ajout de types non nullables à TS, voir https://github.com/Microsoft/TypeScript/issues/185

Qu'il suffise de dire, aussi bien que les types non nullables sont la grande majorité des langages populaires d'aujourd'hui n'ont pas de types non nullables par défaut (c'est là que la fonctionnalité brille vraiment) ou aucune fonctionnalité généralisée de non-nullabilité du tout. Et peu, voire aucun, ont tenté de l'ajouter (ou de l'ajouter avec succès) après coup en raison de la complexité et du fait qu'une grande partie de la valeur de la non-nullabilité réside dans le fait qu'elle est la valeur par défaut (similaire à l'immuabilité). Cela ne veut pas dire que nous ne considérons pas les possibilités ici, mais je n'appellerais pas cela non plus une fonctionnalité obligatoire.

En fait, autant que je manque de type non nul, la vraie fonctionnalité qui me manque dans le flux est la capture générique, le fait que ts résolve chaque générique en {} rend vraiment difficile à utiliser avec une construction fonctionnelle, en particulier le currying.

Personnellement, la capture générique et la non-nullabilité sont des cibles de valeur _high_ de Flow. Je vais lire l'autre fil, mais je voulais aussi jeter mon 2c ici.

J'ai parfois le sentiment que l'avantage d'ajouter la non-nullabilité vaut presque n'importe quel coût. Il s'agit d'une condition d'erreur de probabilité si élevée et, bien que la possibilité de nullité par défaut affaiblisse la valeur intégrée pour le moment, TypeScript n'a même pas la capacité de discuter de la nullabilité en supposant simplement que c'est le cas partout.

Je voudrais annoter chaque variable que je pourrais trouver comme non nullable en un clin d'œil.

Il y a pas mal de fonctionnalités cachées dans flow, non documentées dans le site de flow. Y compris SuperType lié et type existentiel

http://sitr.us/2015/05/31/advanced-features-in-flow.html

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