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 {}
.
Object
con flujoDel 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_.
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
}
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
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>
El flujo, en general, intenta inferir más tipos que TypeScript.
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
.
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.
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
mixed
y any
, ya que mixed
es el equivalente a {}
no hay necesidad, por ejemplo.Object
._No dudes en notificarme si olvidé algo, intentaré actualizar el problema.
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.
Esto es incorrecto.mixed
no tiene propiedades, ni siquiera las propiedades heredadas de Object.prototype que tiene {}
(# 1108)
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?
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?
string!
lugar del modificador que acepta valores nulos de Flow ?string
. Veo tres problemas con esto:undefined
? _ (Flow evita este problema) _mixed
y Object
.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.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
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.