Typescript: Comparação com o Facebook Flow Type System

Criado em 25 nov. 2014  ·  31Comentários  ·  Fonte: microsoft/TypeScript

Aviso: Este problema não tem por objetivo provar que o fluxo é melhor ou pior do que o TypeScript, não quero criticar os incríveis trabalhos de ambas as equipes, mas listar as diferenças no sistema de tipos Flow e TypeScript e tentar avaliar qual recurso poderia melhorar o TypeScript.

Além disso, não falarei sobre os recursos ausentes no Flow, uma vez que o objetivo é o de melhorar o TypeScript.
Finalmente, este tópico é apenas sobre o sistema de tipos e não sobre os recursos es6 / es7 suportados.

mixed e any

Do documento de fluxo:

  • misturado: o "supertipo" de todos os tipos. Qualquer tipo pode fluir para uma mistura.
  • any: o tipo "dinâmico". Qualquer tipo pode fluir para qualquer e vice-versa

Basicamente, isso significa que com o fluxo any é o equivalente a TypeScript any e mixed é o equivalente a TypeScript {} .

O tipo Object com fluxo

Do documento de fluxo:

Use mixed para anotar um local que pode receber qualquer coisa, mas não use Object em vez disso! É confuso ver tudo como um objeto, e se por acaso você quiser dizer "qualquer objeto", existe uma maneira melhor de especificar isso, assim como existe uma maneira de especificar "qualquer função".

Com TypeScript Object é o equivalente a {} e aceita qualquer tipo, com Flow Object é o equivalente a {} mas é diferente de mixed , ele só aceita Objetos (e não outros tipos primitivos como 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

Neste exemplo, o parâmetro de logObjectKeys está marcado com o tipo Object , para TypeScript que é o equivalente a {} e por isso aceitará qualquer tipo, como number no caso da segunda chamada logObjectKeys(3) .
Com o Flow, outros tipos primitivos não são compatíveis com Object e, portanto, o verificador de tipo relatará um erro com a segunda chamada logObjectKeys(3) : _number é incompatível com Object_.

Os tipos não são nulos

Do documento de fluxo:

Em JavaScript, null converte implicitamente em todos os tipos primitivos; também é um habitante válido de qualquer tipo de objeto.
Em contraste, Flow considera nulo como um valor distinto que não faz parte de nenhum outro tipo.

consulte a seção Documento de fluxo

Como o documento de fluxo é bastante completo, não descreverei esse recurso em detalhes, apenas tenha em mente que ele está forçando o desenvolvedor a ter todas as variáveis ​​a serem inicializadas ou marcadas como anuláveis, exemplos:

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
}

No entanto, como para o recurso de proteção do tipo TypeScript, o fluxo compreende a verificação não 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 Intersecção

consulte a seção Documento de fluxo
consulte o problema do TypeScript correspondente # 1256

Como os tipos de união de suporte de fluxo TypeScript, ele também oferece suporte a uma nova maneira de combinar tipos: Tipos de interseção.
Com o objeto, os tipos de interseção são como declarar mixins:

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

AB tem para o tipo { foo: string; bar : string;} ;

Para funções, é equivalente a declarar sobrecarga:

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

é equivalente a :

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

Captura de resolução genérica

Considere o seguinte exemplo 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);

Com TypeScript promisifiedIdentity terá por tipo:

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

Com fluxo promisifiedIdentity terá por tipo:

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

Inferência de tipo

O fluxo em geral tenta inferir mais tipo do que TypeScript.

Inferência de parâmetros

Vamos dar uma olhada neste exemplo:

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

Com TypeScript, nenhum erro é relatado, com flow a última chamada de logLength resultará em um erro porque number não tem uma propriedade length .

Alterações de tipo inferidas com o uso

Com o fluxo, a menos que você digite expressamente sua variável, o tipo dessa variável mudará com o uso desta variável:

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

Neste exemplo, x inicialmente tem string tipo, mas quando atribuído a um número, o tipo foi alterado para number .
Com o texto datilografado, a atribuição x = 5 resultaria em um erro, pois x foi atribuído anteriormente a string e seu tipo não pode ser alterado.

Inferência de tipos de união

Outra diferença é que o Flow propaga a inferência de tipo para trás para ampliar o tipo inferido em uma união de tipo. Este exemplo é do facebook / flow # 67 (comentário)

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
}

("corretamente" é da postagem original.)
Como o fluxo detectou que a variável a poderia ter o tipo B ou C tipo dependendo de uma declaração condicional, agora é inferido para B | C , e assim o A instrução a.x não resulta em erro, pois ambos os tipos têm uma propriedade x , se tivéssemos tentado acessar a propriedade z e o erro seria gerado.

Isso significa que o seguinte também será compilado.

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

  • Atualizada a seção mixed e any , já que mixed é o equivalente a {} não há necessidade de exemplo.
  • Adicionada seção para o tipo Object .
  • Adicionada seção sobre inferência de tipo

_Sinta-se à vontade para avisar se eu esquecer algo, tentarei atualizar o problema._

Question

Comentários muito úteis

Pessoalmente, a captura genérica e a não nulidade são alvos de _alto_ valor do Flow. Vou ler o outro tópico, mas também queria colocar meu 2c aqui.

Às vezes, sinto que o benefício de adicionar a não nulidade vale quase qualquer custo. É uma condição de erro de alta probabilidade e, embora a nulidade padrão enfraqueça o valor integrado agora, o TypeScript não tem a capacidade de até mesmo discutir a nulidade simplesmente assumindo que é o caso em todos os lugares.

Gostaria de anotar todas as variáveis ​​que pudesse encontrar como não anuláveis ​​em um piscar de olhos.

Todos 31 comentários

Isso é interessante e um bom ponto de partida para mais discussão. Você se importa se eu fizer algumas alterações de edição de texto na postagem original para maior clareza?

Coisas inesperadas no Flow (atualizarei este comentário conforme eu o investigue mais)

Inferência de tipo de argumento de função ímpar:

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

Sem inferência de tipo de literais de objeto:

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

Isso é interessante e um bom ponto de partida para mais discussão. Você se importa se eu fizer algumas alterações de edição de texto na postagem original para maior clareza?

Sinta-se à vontade Como eu disse, o objetivo é tentar investir no sistema de tipo de fluxo para ver se alguns recursos cabem no TypeScript.

@RyanCavanaugh Acho que o último exemplo:

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

É um bug relacionado ao algoritmo de verificação de nulos, vou relatá-lo.

É

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

Equivalente a

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

Ou pode ser?

@ Davidhanson90 realmente não:

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

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

neste exemplo, o fluxo é incapaz de saber qual tipo no tipo de união func é, portanto, ele relata o erro em ambos os casos

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

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

func tem ambos os tipos, então ambas as chamadas são válidas.

Existe alguma diferença observável entre {} no TypeScript e mixed no Flow?

@RyanCavanaugh Eu realmente não sei, depois de pensar, acho que é a mesma coisa ainda pensar nisso.

mixed não tem propriedades, nem mesmo as propriedades herdadas de Object.prototype que {} tem (# 1108) Isso está errado.

Outra diferença é que o Flow propaga a inferência de tipo para trás para ampliar o tipo inferido em uma união de tipo. Este exemplo é 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
}

("corretamente" é da postagem original.)

Isso significa que o seguinte também será compilado.

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: testou o segundo trecho e realmente compila.
Edição 2: conforme apontado por @fdecampredon abaixo, o if (true) { } torno da segunda atribuição é necessário para que o Flow deduza o tipo como string | number . Sem o if (true) , é inferido como number .

Você gosta desse comportamento? Seguimos por esse caminho quando discutimos os tipos de união e o valor é duvidoso. Só porque o sistema de tipos agora tem a capacidade de modelar tipos com vários estados possíveis, não significa que é desejável usá-los em todos os lugares. Aparentemente, você escolheu usar uma linguagem com um verificador de tipo estático porque deseja erros do compilador quando comete erros, não apenas porque gosta de escrever anotações de tipo;) Ou seja, a maioria das linguagens dá um erro em um exemplo como este (particularmente o segundo) não por falta de uma maneira de modelar o espaço de tipo, mas porque eles realmente acreditam que isso é um erro de codificação (por razões semelhantes, muitos evitam o suporte a muitas operações implícitas de conversão / conversão).

Pela mesma lógica, eu esperaria este comportamento:

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

mas eu realmente não quero esse comportamento.

@danquirk: Eu concordo com você que inferir o tipo de união automaticamente em vez de relatar um erro não é um comportamento de que gosto.
Mas acho que isso vem da filosofia de fluxo, mais do que uma linguagem real, a equipe de fluxo tenta criar simplesmente um verificador de tipo, seu objetivo final é ser capaz de fazer código 'mais seguro' sem quaisquer anotações de tipo. Isso leva a ser menos rígido.

O rigor exato é até discutível, dados os efeitos colaterais desse tipo de comportamento. Muitas vezes, é apenas adiar um erro (ou ocultá-lo inteiramente). Nossas regras de inferência de tipo antigas para argumentos de tipo refletiam muito uma filosofia semelhante. Em caso de dúvida, inferimos {} para um parâmetro de tipo em vez de torná-lo um erro. Isso significava que você poderia fazer algumas coisas estúpidas e ainda fazer alguns conjuntos mínimos de comportamentos com segurança no resultado (a saber, coisas como toString ). A lógica é que algumas pessoas fazem coisas estúpidas em JS e devemos tentar permitir o que pudermos. Mas, na prática, a maioria das inferências para {} eram, na verdade, apenas erros, fazendo você esperar até a primeira vez que pontuou uma variável do tipo T para perceber que era {} (ou também um tipo de união inesperado) e rastrear para trás era, na melhor das hipóteses, irritante. Se você nunca perdeu o controle (ou nunca retornou algo do tipo T), não percebeu o erro até o tempo de execução, quando algo explodiu (ou pior, dados corrompidos). Similarmente:

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

Qual é o erro aqui? Está realmente passando x para foo ? Ou estava reatribuindo x um valor de um tipo completamente diferente daquele com o qual foi inicializado? Com que frequência as pessoas realmente fazem esse tipo de reinicialização intencionalmente em vez de pisar em algo acidentalmente? Em qualquer caso, ao inferir um tipo de união para x você pode realmente dizer que o sistema de tipos era menos estrito no geral se ainda resultasse em um (pior) erro? Esse tipo de inferência só é menos estrita se você nunca fizer nada particularmente significativo com o tipo resultante, o que geralmente é muito raro.

Indiscutivelmente, deixando null e undefined atribuíveis a qualquer tipo, ocultar erros da mesma maneira, na maioria das vezes uma variável digitada com algum tipo e ocultando um null levará a um erro em tempo de execução.

Uma parte não insignificante do marketing da Flow é baseada no fato de que seu verificador de tipos dá mais sentido ao código em lugares onde o TS inferiria any . Sua filosofia é que você não precisa adicionar anotações para fazer o compilador inferir tipos. É por isso que seu dial de inferência está voltado para uma configuração muito mais permissiva do que o TypeScript.

Tudo se resume a se alguém tem a expectativa de que var x = new B(); x = new C(); (onde B e C derivam de A) devem compilar ou não, e se sim, como deveria ser inferido?

  1. Não deve compilar.
  2. Deve compilar e ser inferido como o tipo de base mais derivado comum às hierarquias de tipo de B e C - A. Para o exemplo de número e string, seria {}
  3. Deve compilar e ser inferido como B | C .

Atualmente, o TS faz (1) e o Flow (3). Eu prefiro (1) e (2) muito mais a (3).

Eu queria adicionar exemplos do @Arnavion ao problema original, mas depois de brincar um pouco, percebi que as coisas
Neste exemplo:

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

Agora :

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

após este exemplo, x é string | number
E se eu fizer:

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

Recebi um erro na linha 1 dizendo: myFile.js line 1 string this type is incompatible with myFile.js line 5 number

Eu ainda preciso descobrir a lógica aqui ....

Há também um ponto interessante sobre o fluxo que esqueci:

function test(t: Object) { }

test('string'); //error

Basicamente, 'Objeto' não é compatível com outro tipo primitivo, acho que faz sentido.

A 'captura de resolução genérica' é definitivamente um recurso obrigatório para TS!

@fdecampredon Sim, você está certo. Com var x = "5"; x = 5; x o tipo inferido é atualizado para number . Ao adicionar if (true) { } torno da segunda atribuição, o typechecker é levado a assumir que qualquer atribuição é válida, razão pela qual o tipo inferido é atualizado para number | string vez disso.

O erro que você obtém myFile.js line 1 string this type is incompatible with myFile.js line 5 number está correto, uma vez que number | string não suporta o operador * (as únicas operações permitidas em um tipo de união são a interseção de todas as operações em todos os tipos de a União). Para verificar isso, você pode alterá-lo para x += 5 e verá a compilação.

Eu atualizei o exemplo em meu comentário para ter o if (true)

A 'captura de resolução genérica' é definitivamente um recurso obrigatório para TS!

+1

@Arnavion , não sei por que você prefere {} vez de B | C . Inferir B | C amplia o conjunto de programas que fazem a verificação do tipo sem comprometer a correção, o que é uma propriedade geralmente desejável dos sistemas de tipo.

O exemplo

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

já typechecks no compilador atual, exceto que T é inferido como sendo {} vez de string | number . Isso não compromete a correção, mas falando de maneira geral, é menos útil.

Inferir number | string vez de {} não parece problemático para mim. Nesse caso particular, ele não amplia o conjunto de programas válidos, no entanto, se os tipos compartilham a estrutura, o sistema de tipos percebendo isso e tornando válidos alguns métodos e / ou propriedades extras parece apenas uma melhoria.

Inferir B | C amplia o conjunto de programas que fazem a verificação sem comprometer a correção

Acho que permitir a operação + em algo que pode ser uma string ou um número está comprometendo a correção, uma vez que as operações não são nada semelhantes entre si. Não é como a situação em que a operação pertence a uma classe base comum (minha opção 2) - nesse caso, você pode esperar alguma semelhança.

O operador + não poderia ser chamado, pois teria duas sobrecargas incompatíveis - uma em que ambos os argumentos são números e outra em que ambos são strings. Desde B | C é mais estreito do que string e número, não seria permitido como argumento em nenhuma das sobrecargas.

Exceto que as funções são bivariadas em seus argumentos, então isso pode ser um problema?

Eu pensei que, como var foo: string; console.log(foo + 5); console.log(foo + document); compila, o operador string + permitia qualquer coisa no lado direito, então string | number teria + <number> como uma operação válida. Mas você está certo:

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

Muitos dos comentários se concentraram na ampliação automática de tipos no Flow. Em ambos os casos, você pode ter o comportamento desejado adicionando uma anotação. No TS, você ampliaria explicitamente na declaração: var x: number|string = 5; e no Fluxo você restringiria na declaração: var x: number = 5; . Acho que o caso que não requer uma declaração de tipo deve ser aquele que as pessoas usam com mais frequência. Em meus projetos, eu esperaria que var x = 5; x = 'five'; fosse um erro com mais frequência do que um tipo de união. Então eu diria que TS fez a inferência certa sobre este.

Quanto aos recursos do Flow que considero os mais valiosos?

  1. Tipos não nulos
    Acho que este tem um alto potencial de redução de bugs. Para compatibilidade com as definições de TS existentes, imagino-o mais como um modificador não nulo string! em vez do modificador anulável de Flow ?string . Vejo três problemas com isso:
    Como lidar com a inicialização dos membros da classe? _ (provavelmente eles têm que ser atribuídos no ctor e se eles podem escapar do ctor antes da atribuição, eles são considerados anuláveis) _
    Como lidar com undefined ? _ (Fluxo evita este problema) _
    Ele pode funcionar sem muitas declarações de tipo explícitas?
  2. Diferença entre mixed e Object .
    Porque, ao contrário dos tipos primitivos do C #, não podem ser usados ​​em todos os lugares em que um objeto pode. Tente Object.keys(3) em seu navegador e você obterá um erro. Mas isso não é crítico, pois acho que os casos extremos são poucos.
  3. Captura de resolução genérica
    O exemplo simplesmente faz sentido. Mas não posso dizer que estou escrevendo muito código que se beneficiaria com isso. Talvez ajude com bibliotecas funcionais como o Underscore?

Sobre a inferência de tipo de união automática: Presumo que a "inferência de tipo" seja restrita à declaração de tipo. Um mecanismo que infere implicitamente uma declaração de tipo omitida. Curta := no Go. Não sou um teórico de tipo, mas pelo que entendi, a inferência de tipo é um passo do compilador que adiciona uma anotação de tipo explícita a cada declaração de variável implícita (ou argumento de função), inferida do tipo de expressão a partir da qual está sendo atribuída. Pelo que eu sei, é assim que funciona para ... bem ... todos os outros tipos de mecanismo de inferência que existem. C #, Haskell, Go, todos eles funcionam dessa maneira. Ou não?

Eu entendo o argumento sobre permitir que o JS da vida real use a semântica do TS ditar, mas este é talvez um bom ponto para seguir outras linguagens, em vez disso. Afinal, os tipos são a única diferença que define entre JS e TS.

Gosto de muitas das ideias do Flux, mas esta, bem, se é realmente feita desta forma ... é estranho.

Os tipos não nulos parecem um recurso obrigatório para um sistema de tipos moderno. Seria fácil adicionar ao ts?

Se você quiser uma leitura leve sobre as complexidades de adicionar tipos não anuláveis ​​ao TS, consulte https://github.com/Microsoft/TypeScript/issues/185

É suficiente dizer que, por mais legais que os tipos não anuláveis ​​sejam, a grande maioria das linguagens populares hoje não tem tipos não anuláveis ​​por padrão (que é onde o recurso realmente brilha) ou qualquer recurso generalizado de não anulabilidade. E poucos, se houver, tentaram adicioná-lo (ou adicionaram-no com sucesso) após o fato devido à complexidade e ao fato de que muito do valor da não nulidade reside em ser o padrão (semelhante à imutabilidade). Isso não quer dizer que não estamos considerando as possibilidades aqui, mas também não o chamaria de um recurso obrigatório.

Na verdade, por mais que eu sinta falta do tipo não nulo, o recurso real que sinto falta do fluxo é a captura genérica, o fato de que resolve todos os genéricos para {} torna realmente difícil de usar com alguma construção funcional, especialmente currying.

Pessoalmente, a captura genérica e a não nulidade são alvos de _alto_ valor do Flow. Vou ler o outro tópico, mas também queria colocar meu 2c aqui.

Às vezes, sinto que o benefício de adicionar a não nulidade vale quase qualquer custo. É uma condição de erro de alta probabilidade e, embora a nulidade padrão enfraqueça o valor integrado agora, o TypeScript não tem a capacidade de até mesmo discutir a nulidade simplesmente assumindo que é o caso em todos os lugares.

Gostaria de anotar todas as variáveis ​​que pudesse encontrar como não anuláveis ​​em um piscar de olhos.

Existem muitos recursos ocultos no fluxo, não documentados no site do fluxo. Incluindo SuperType vinculado e tipo existencial

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

Esta página foi útil?
0 / 5 - 0 avaliações