Typescript: Comparación con el sistema de tipo de flujo de Facebook

Creado en 25 nov. 2014  ·  31Comentarios  ·  Fuente: microsoft/TypeScript

Descargo de responsabilidad: este problema no tiene el propósito de demostrar que el flujo es mejor o peor que TypeScript, no quiero criticar los increíbles trabajos de ambos equipos, sino enumerar las diferencias en el sistema de tipos Flow y TypeScript y tratar de evaluar qué característica podría mejorar TypeScript.

Además, no hablaré sobre las funciones que faltan en Flow, ya que el propósito es mejorar TypeScript.
Finalmente, este tema trata solo sobre el sistema de tipos y no sobre las características compatibles de es6 / es7.

mixed y any

Del documento de flujo:

  • mixto: el "supertipo" de todos los tipos. Cualquier tipo puede fluir hacia una mezcla.
  • cualquiera: el tipo "dinámico". Cualquier tipo puede fluir hacia cualquiera y viceversa.

Básicamente, eso significa que con el flujo any es el equivalente de TypeScript any y mixed es el equivalente de TypeScript {} .

El tipo Object con flujo

Del documento de flujo:

Use mixto para anotar una ubicación que pueda tomar cualquier cosa, ¡pero no use Objeto en su lugar! Es confuso ver todo como un objeto, y si por casualidad te refieres a "cualquier objeto", hay una mejor manera de especificar eso, así como hay una manera de especificar "cualquier función".

Con TypeScript Object es el equivalente de {} y acepta cualquier tipo, con Flow Object es el equivalente de {} pero es diferente de mixed , solo acepta Object (y no otros tipos primitivos como string , number , boolean o 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

En este ejemplo, el parámetro de logObjectKeys está etiquetado con el tipo Object , para TypeScript que es el equivalente de {} y, por lo tanto, aceptará cualquier tipo, como number en el caso de la segunda llamada logObjectKeys(3) .
Con Flow, otros tipos primitivos no son compatibles con Object por lo que el verificador de tipos informará un error con la segunda llamada logObjectKeys(3) : _number es incompatible con Object_.

El tipo no es nulo

Del documento de flujo:

En JavaScript, null se convierte implícitamente a todos los tipos primitivos; también es un habitante válido de cualquier tipo de objeto.
Por el contrario, Flow considera que nulo es un valor distinto que no forma parte de ningún otro tipo.

ver la sección de documentos de flujo

Dado que el documento de flujo es bastante completo, no describiré esta característica en detalle, solo tenga en cuenta que está obligando al desarrollador a tener todas las variables para inicializar, o marcar como anulables, ejemplos:

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
}

Sin embargo, al igual que para la función de protección de tipo de TypeScript, el flujo comprende la verificación no nula:

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
}

Tipo de intersección

ver la sección de documentos de flujo
consulte el número 1256 de Correspondin TypeScript

Al igual que los tipos de unión de soporte de flujo de TypeScript, también admite una nueva forma de combinar tipos: Tipos de intersección.
Con objeto, los tipos de intersección es como declarar mixins:

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

AB tiene por tipo { foo: string; bar : string;} ;

Para funciones equivale a declarar sobrecarga:

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

es equivalente a :

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

Captura de resolución genérica

Considere el siguiente ejemplo de TypeScript:

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

var promisifiedIdentity = promisify(identity);

Con TypeScript promisifiedIdentity tendrá por tipo:

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

Con el flujo promisifiedIdentity tendrá por tipo:

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

Inferencia de tipo

El flujo, en general, intenta inferir más tipos que TypeScript.

Inferencia de parámetros

Echemos un vistazo a este ejemplo:

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

Con TypeScript, no se informan errores, con flow la última llamada de logLength dará como resultado un error porque number no tiene una propiedad length .

El tipo inferido cambia con el uso

Con el flujo, a menos que escriba expresamente su variable, el tipo de esta variable cambiará con el uso de esta 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

En este ejemplo, x tiene inicialmente el tipo string , pero cuando se asigna a un número, el tipo se ha cambiado a number .
Con mecanografiado, la asignación x = 5 resultaría en un error ya que x se asignó previamente a string y su tipo no puede cambiar.

Inferencia de tipos de unión

Otra diferencia es que Flow propaga la inferencia de tipos hacia atrás para ampliar el tipo inferido en una unión de tipos. Este ejemplo es de facebook / flow # 67 (comentario)

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
}

("correctamente" es de la publicación original).
Dado que el flujo detectó que la variable a podría tener el tipo B o el tipo C dependiendo de una declaración condicional, ahora se infiere como B | C , por lo que declaración a.x no da como resultado un error ya que ambos tipos tienen una propiedad x , si hubiéramos intentado acceder a la propiedad z y se habría generado un error.

Esto significa que lo siguiente también se compilará.

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

Editar

  • Se actualizó la sección mixed y any , ya que mixed es el equivalente a {} no hay necesidad, por ejemplo.
  • Sección agregada para el tipo Object .
  • Sección agregada sobre inferencia de tipos

_No dudes en notificarme si olvidé algo, intentaré actualizar el problema.

Question

Comentario más útil

Personalmente, la captura genérica y la no nulabilidad son objetivos de _alto_ valor de Flow. Leeré el otro hilo, pero también quería lanzar mi 2c aquí.

A veces siento que el beneficio de agregar no nulabilidad vale casi cualquier costo. Es una condición de error de alta probabilidad y, si bien la posibilidad de nulabilidad predeterminada debilita el valor incorporado en este momento, TypeScript carece de la capacidad de discutir la nulabilidad simplemente asumiendo que es el caso en todas partes.

Anotaría cada variable que pudiera encontrar como no anulable en un latido.

Todos 31 comentarios

Esto es interesante y un buen punto de partida para una mayor discusión. ¿Le importa si hago algunos cambios de corrección de estilo en la publicación original para mayor claridad?

Cosas inesperadas en Flow (actualizaré este comentario a medida que lo investigue más)

Inferencia de tipo de argumento de función impar:

/** 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 (??)

Sin inferencia de tipos a partir de objetos literales:

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

Esto es interesante y un buen punto de partida para una mayor discusión. ¿Le importa si hago algunos cambios de corrección de estilo en la publicación original para mayor claridad?

Siéntase libre Como dije, el propósito es intentar invertir en un sistema de tipo de flujo para ver si algunas características podrían encajar en TypeScript.

@RyanCavanaugh Supongo que el último ejemplo:

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

Es un error relacionado con su algoritmo de verificación nula, lo informaré.

Es

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

Equivalente a

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

¿O podría ser?

@ Davidhanson90 no realmente:

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

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

en este ejemplo, el flujo no puede saber qué tipo en el tipo de unión func es, por lo que informa un error en ambos casos

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

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

func tiene ambos tipos, por lo que ambas llamadas son válidas.

¿Hay alguna diferencia observable entre {} en TypeScript y mixed en Flow?

@RyanCavanaugh Realmente no lo sé después de pensarlo, creo que es más o menos lo mismo todavía pensando en ello.

mixed no tiene propiedades, ni siquiera las propiedades heredadas de Object.prototype que tiene {} (# 1108) Esto es incorrecto.

Otra diferencia es que Flow propaga la inferencia de tipos hacia atrás para ampliar el tipo inferido en una unión de tipos. Este ejemplo es 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
}

("correctamente" es de la publicación original).

Esto significa que lo siguiente también se compilará.

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

Editar: probé el segundo fragmento y realmente se compila.
Edición 2: como señala @fdecampredon a continuación, se if (true) { } alrededor de la segunda asignación para que Flow infiera el tipo como string | number . Sin if (true) , se infiere como number .

¿Te gusta este comportamiento? Seguimos esta ruta cuando discutimos los tipos de sindicatos y el valor es dudoso. El hecho de que el sistema de tipos ahora tenga la capacidad de modelar tipos con múltiples estados posibles no significa que sea deseable usarlos en todas partes. Aparentemente ha elegido usar un lenguaje con un verificador de tipo estático porque desea errores de compilación cuando comete errores, no solo porque le gusta escribir anotaciones de tipo;) Es decir, la mayoría de los lenguajes dan un error en un ejemplo como este (particularmente el segundo) no por falta de una forma de modelar el espacio de tipos, sino porque realmente creen que se trata de un error de codificación (por razones similares, muchos evitan admitir muchas operaciones de conversión / conversión implícitas).

Por la misma lógica, esperaría este comportamiento:

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

pero realmente no quiero ese comportamiento.

@danquirk Estoy de acuerdo contigo en que inferir el tipo de unión automáticamente en lugar de informar un error no es un comportamiento que me guste.
Pero creo que eso viene de la filosofía de flujo, más que de un lenguaje real, el equipo de flujo intenta crear simplemente un verificador de tipos, su objetivo final es poder hacer código 'más seguro' sin ningún tipo de anotaciones. Esto lleva a ser menos estricto.

El rigor exacto es incluso discutible dado el impacto en los efectos de este tipo de comportamiento. A menudo se trata simplemente de posponer un error (u ocultar uno por completo). Nuestras antiguas reglas de inferencia de tipos para argumentos de tipo reflejaban en gran medida una filosofía similar. En caso de duda, inferimos {} para un parámetro de tipo en lugar de convertirlo en un error. Esto significaba que podía hacer algunas cosas tontas y aún hacer un conjunto mínimo de comportamientos de manera segura en el resultado (es decir, cosas como toString ). La razón es que algunas personas hacen cosas tontas en JS y deberíamos intentar permitir lo que podamos. Pero en la práctica, la mayoría de las inferencias de {} fueron en realidad solo errores, y lo hicieron esperar hasta la primera vez que punteó una variable de tipo T para darse cuenta de que era {} (o igualmente un tipo de unión inesperado) y luego rastrear hacia atrás fue molesto en el mejor de los casos. Si nunca lo puntuó (o nunca devolvió algo de tipo T), no notó el error en absoluto hasta el tiempo de ejecución cuando algo explotó (o peor aún, datos corruptos). Similar:

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

¿Cuál es el error aquí? ¿Realmente está pasando de x a foo ? ¿O estaba reasignando x un valor de un tipo completamente diferente al que se inicializó? ¿Con qué frecuencia la gente realmente intencionalmente hace ese tipo de reinicialización en lugar de pisar algo accidentalmente? En cualquier caso, al inferir un tipo de unión para x puede realmente decir que el sistema de tipos fue menos estricto en general si aún así resultó en un error (peor)? Este tipo de inferencia es solo menos estricta si nunca hace nada particularmente significativo con el tipo resultante, que generalmente es bastante raro.

Podría decirse que dejar null y undefined asignables a cualquier tipo oculta los errores de la misma manera, la mayoría de las veces una variable escrita con algún tipo y ocultando un null conducirá a una error en tiempo de ejecución.

Una parte no insignificante del marketing de Flow se basa en el hecho de que su comprobador de tipos tiene más sentido del código en lugares donde TS inferiría any . Su filosofía es que no debería ser necesario agregar anotaciones para que el compilador infiera tipos. Es por eso que su dial de inferencia está en una configuración mucho más permisiva que la de TypeScript.

Todo se reduce a si alguien tiene la expectativa de que var x = new B(); x = new C(); (donde B y C derivan de A) se compile o no, y si lo hace, ¿cómo se debe inferir?

  1. No debería compilar.
  2. Debe compilarse e inferirse como el tipo base más derivado común a las jerarquías de tipos de B y C - A. Para el ejemplo de número y cadena sería {}
  3. Debería compilarse e inferirse como B | C .

TS actualmente hace (1) y Flow hace (3). Prefiero (1) y (2) mucho más que (3).

Quería agregar ejemplos de @Arnavion al número original, pero después de jugar un poco me di cuenta de que las cosas eran más extrañas de lo que entendíamos.
En este ejemplo :

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

Ahora :

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

después de este ejemplo x es string | number
Y si lo hago:

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

Recibí un error en la línea 1 que decía: myFile.js line 1 string this type is incompatible with myFile.js line 5 number

Todavía necesito descifrar la lógica aquí ...

También hay un punto interesante sobre el flujo que olvidé:

function test(t: Object) { }

test('string'); //error

Básicamente 'Objeto' no es compatible con otro tipo primitivo, creo que uno tiene sentido.

¡La 'captura de resolución genérica' es definitivamente una característica imprescindible para TS!

@fdecampredon Sí, tienes razón. Con var x = "5"; x = 5; x, el tipo inferido se actualiza a number . Al agregar if (true) { } alrededor de la segunda asignación, se engaña al verificador de tipos para que asuma que cualquiera de las asignaciones es válida, por lo que el tipo inferido se actualiza a number | string .

El error que obtiene myFile.js line 1 string this type is incompatible with myFile.js line 5 number es correcto, ya que number | string no admite el operador * (las únicas operaciones permitidas en un tipo de unión son la intersección de todas las operaciones en todos los tipos de la Union). Para verificar esto, puede cambiarlo a x += 5 y verá que se compila.

Actualicé el ejemplo en mi comentario para tener el if (true)

¡La 'captura de resolución genérica' es definitivamente una característica imprescindible para TS!

+1

@Arnavion , no estoy seguro de por qué preferirías {} sobre B | C . Inferir B | C amplía el conjunto de programas que verifican el tipo sin comprometer la corrección, lo que afaik es una propiedad generalmente deseable de los sistemas de tipos.

El ejemplo

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

ya se verifica el tipo en el compilador actual, excepto que se infiere que T es {} lugar de string | number . Esto no compromete la corrección, pero en términos generales es menos útil.

Inferir number | string lugar de {} no me parece problemático. En ese caso particular, no amplía el conjunto de programas válidos, sin embargo, si los tipos comparten la estructura, el sistema de tipos se da cuenta de eso y hace que algunos métodos y / o propiedades adicionales sean válidos parece solo una mejora.

Inferir B | C amplía el conjunto de programas que revisan el tipo sin comprometer la corrección

Creo que permitir la operación + en algo que puede ser una cadena o un número compromete la corrección, ya que las operaciones no son similares entre sí en absoluto. No es como la situación en la que la operación pertenece a una clase base común (mi opción 2); en ese caso, puede esperar alguna similitud.

El operador + no sería invocable, ya que tendría dos sobrecargas incompatibles: una donde ambos argumentos son números y otra donde ambos son cadenas. Dado que B | C es más estrecho que la cadena y el número, no se permitiría como argumento en ninguna de las sobrecargas.

¿Excepto que las funciones son bivariantes con sus argumentos, por lo que eso podría ser un problema?

Pensé que, dado que var foo: string; console.log(foo + 5); console.log(foo + document); compila, el operador string + permitía cualquier cosa en el lado derecho, entonces string | number tendría + <number> como una operación válida. Pero estás en lo correcto:

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

Muchos de los comentarios se han centrado en la ampliación automática de tipos en Flow. En ambos casos, puede tener el comportamiento que desee agregando una anotación. En TS, se ampliaría explícitamente en la declaración: var x: number|string = 5; y en Flow se restringiría en la declaración: var x: number = 5; . Creo que el caso que no requiere una declaración de tipo debería ser el que la gente usa con más frecuencia. En mis proyectos, esperaría que var x = 5; x = 'five'; sea ​​un error con más frecuencia que un tipo de unión. Entonces yo diría que TS hizo la inferencia correcta en este caso.

¿En cuanto a las funciones de Flow que creo que son las más valiosas?

  1. Tipos no nulos
    Creo que este tiene un potencial muy alto para reducir errores. Por compatibilidad con las definiciones de TS existentes, lo imagino más como un modificador no nulo string! lugar del modificador que acepta valores nulos de Flow ?string . Veo tres problemas con esto:
    ¿Cómo manejar la inicialización de los miembros de la clase? _ (probablemente tienen que ser asignados en el ctor y si pueden escapar del ctor antes de la asignación se consideran anulables) _
    ¿Cómo manejar undefined ? _ (Flow evita este problema) _
    ¿Puede funcionar sin muchas declaraciones de tipo explícitas?
  2. Diferencia entre mixed y Object .
    Porque, a diferencia de C #, los tipos primitivos no se pueden usar en todos los lugares donde se puede usar un objeto. Pruebe Object.keys(3) en su navegador y obtendrá un error. Pero esto no es crítico ya que creo que los casos extremos son pocos.
  3. Captura de resolución genérica
    El ejemplo tiene sentido. Pero no puedo decir que esté escribiendo mucho código que se beneficiaría de eso. ¿Quizás ayude con bibliotecas funcionales como Underscore?

Sobre la inferencia de tipo de unión automática: supongo que la "inferencia de tipo" está restringida a la declaración de tipo. Un mecanismo que infiere implícitamente una declaración de tipo omitida. Como := en Go. No soy un teórico de tipos, pero hasta donde tengo entendido, la inferencia de tipos es un pase de compilador que agrega una anotación de tipo explícita a cada declaración de variable implícita (o argumento de función), inferida del tipo de expresión desde la que se está asignando. Hasta donde yo sé, así es como funciona para ... bueno ... cualquier otro tipo de mecanismo de inferencia que existe. C #, Haskell, Go, todos funcionan de esta manera. ¿O no?

Entiendo el argumento de dejar que el uso de JS de la vida real dicte la semántica de TS, pero este es quizás un buen punto para seguir otros lenguajes. Los tipos son la única diferencia definitoria entre JS y TS, después de todo.

Me gustan muchas de las ideas de Flux, pero esta, bueno, si en realidad se hace de esta manera ... es simplemente extraño.

Los tipos no nulos parecen una característica obligatoria para un sistema de tipos moderno. ¿Sería fácil agregarlos a los ts?

Si desea una lectura ligera sobre las complejidades de agregar tipos que no aceptan valores NULL a TS, consulte https://github.com/Microsoft/TypeScript/issues/185

Basta con decir que, por más agradables que sean los tipos que no aceptan nulos, la gran mayoría de los lenguajes populares de hoy en día no tienen tipos que no admiten nulos de forma predeterminada (que es donde realmente brilla la característica) o ninguna característica generalizada que no admite nulos. Y pocos, si es que alguno, han intentado agregarlo (o agregarlo con éxito) después del hecho debido a la complejidad y al hecho de que gran parte del valor de la no nulabilidad radica en que es el predeterminado (similar a la inmutabilidad). Esto no quiere decir que no estemos considerando las posibilidades aquí, pero tampoco lo llamaría una característica obligatoria.

En realidad, por mucho que extraño el tipo no nulo, la característica real que extraño del flujo es la captura genérica, el hecho de que resuelva cada genérico en {} hace que sea realmente difícil de usar con alguna construcción funcional, especialmente curry.

Personalmente, la captura genérica y la no nulabilidad son objetivos de _alto_ valor de Flow. Leeré el otro hilo, pero también quería lanzar mi 2c aquí.

A veces siento que el beneficio de agregar no nulabilidad vale casi cualquier costo. Es una condición de error de alta probabilidad y, si bien la posibilidad de nulabilidad predeterminada debilita el valor incorporado en este momento, TypeScript carece de la capacidad de discutir la nulabilidad simplemente asumiendo que es el caso en todas partes.

Anotaría cada variable que pudiera encontrar como no anulable en un latido.

Hay muchas características ocultas en el flujo, no documentadas en el sitio del flujo. Incluyendo SuperType tipo enlazado y existencial

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

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