Flow: Fundamentos para Desenvolvedores JavaScript

Tudo o que você precisa saber para começar com o FlowType.

Eduardo Rabelo
Training Center
10 min readOct 8, 2017

--

Iremos fazer um passo a passo de todas as características básicas.

Examinaremos as características básicas para obter uma melhor compreensão dos fundamentos.

Você pode ver os recursos um a um e trabalhar com este tutorial.

Se você tiver alguma dúvida ou comentário, pode entrar em contato no Twitter

Versão utilizada: Flow v0.55

Instalação e Configuração

Sempre consulte a documentação oficial para uma compreensão mais detalhada: https://flow.org/en/

Para instalação, você pode dar uma olhada nessa página: https://flow.org/en/docs/install/

Você também pode brincar no REPL online: https://flow.org/try/

E pode configurar no seu editor preferido: https://flow.org/en/docs/editors/

Básico

Vamos começar com alguns exemplos bem básicos:

const a = 'foo'
const b = 1
const c = false
const d = [1, 2, 3]
const e = ['a', 'b', 'c']
const f = {id: 1}
const g = null
const h = undefined

Flow oferece uma série de tipos. Para ter uma resposta geral rápida, vamos ser explícitos e definir tipos para as constantes do exemplo acima.

// usando o tipo correto, nenhum erro!
const a : string = 'foo';

Se utilizarmos um tipo diferente:

// oops!
const a : number = 'foo';

Obteremos o seguinte erro:

const a : number = 'foo';
^^^^^ string. This type is incompatible with

Aqui, estamos atribuindo o tipo para fins de aprendizagem, na realidade, você poderia confiar na inferência de tipos do Flow.

Mais exemplos

Vamos analisar outro cenário:

const aTyped : string = 'foo';
const bTyped : number = 1;
const cTyped : boolean = false;
// const d = [1, 2, 3], do primeiro exemplo
const dTyped; // ?

Os três primeiros são relativamente claros. Mas como declaramos d?

Nós podemos usar:

// podemos declarar
const dTyped : number[] = [1, 2, 3]
// ou dessa maneira
const dTyped : Array<number> = [1, 2, 3]

E se continuarmos declarando os tipos:

// veja as variáveis `e`, `f` e `g` no primeiro exemplo
const eTyped : Array<string> = ['a', 'b', 'c'];
const fTyped : Object = {id: 1};
const gTyped : null = null;

E como fica undefined?

No FlowType você pode usar o tipo void para declarar um valor como indefinido:

const h : void = undefined;

Entraremos em mais detalhes à medida que avançarmos e abrangermos valores primitivos com mais detalhes.

Mais de um tipo

Vamos continuar e ver o que mais o FlowType tem para oferecer.

Considere:

const i = 2;
const j = 'foo';

Nos exemplos anteriores, i ej podem ser atribuídos a um tipo primitivo, mas, curiosamente um tipo literal também. Como seria isso?

const iTyped : 2 = 2;
const jTyped : 'foo' = 'foo';

Agora você pode estar se perguntando: qual valor que ganhamos ao usar tipos literais?

Podemos restringir os valores que esperamos:

type ExpectedInput = 1 | 2 | 3const doSomething = (input: ExpectedInput) => {
switch (input) {
case 1: return 'Level 1'
case 2: return 'Level 2'
case 3: return 'Level 3'
}
}
// retorna um erro
doSomething(0);
// funciona corretamente
doSomething(1);

Nossos exemplos estavam mostrando apenas variáveis constantes, até agora.

E como fica let ou var?

As variáveis const não podem ser reatribuídas, o que significa que o FlowType pode inferir no tipo e saber com toda certeza que seu valor nunca mudará.

Isso é diferente com let ou var.

Por exemplo:

let aVar : string = 'foo'// tudo certo
aVar = 'bar';
// ops, um erro!
aVar = 1;

Como podemos ver no exemplo acima, uma vez que você atribui um tipo a uma variável let ou var, qualquer re-atribuição deve ser do mesmo tipo, caso contrário o Flow irá reclamar.

Dê uma olhada no seguinte exemplo:

const i : 3 = 2;
^ number. Expected number literal `3`, got `2` instead

Valores Any e Mixed

As vezes você não pode dizer qual é o tipo exato ou você está convertendo uma base de código existente que não contém tipos.

É aqui que any emixed são úteis.

É importante notar que eles contém diferentes propósitos.

any deve ser usado como último recurso, pois ignora a verificação de tipo.

mixed é útil quando você não pode ter certeza do tipo de entrada, ou seja:

const double = (input: mixed) => {
if (typeof input === 'string') {
return input + ' - ' + input
}
if (Array.isArray(input)) {
return input.concat(input)
}
return input
}
// funciona corretamente
const result = doSomething('foo');

Precisamos refinar a entrada, verificando o tipo e depois retornando um valor apropriado, caso contrário o Flow irá reclamará.

Com any, ignoramos completamente o tipo a ser verificado. Podemos passar qualquer valor para length e nunca receberemos um erro. Como já mencionado, use any como último recurso, se possível.

const length = (input: any) => {
if (typeof input === 'string') {
return input.length
}
if (Array.isArray(input)) {
return input.length
}
return false
}
const bar = length('foo');
const baz = length([1, 2, 3, 4]);
// nenhum erro é retornado aqui, tome cuidado!
const foo = length(1);

Valores Opcionais

As vezes, queremos que algum valor seja opcional. Por exemplo, veja a seguinte função:

const optionalLength = (input: ? string | Array<any>) => {
if (typeof input === 'string') {
return input.length
}
if (Array.isArray(input)) {
return input.length
}
return false
}
optionalLength();
optionalLength(null);
optionalLength(undefined);
optionalLength([1, 2, 3, 4]);
optionalLength('foo');

Como podemos ver, podemos chamar optionalLength com undefined, null, um array ou uma string. Mas, como seria é de se esperar, passar num número causaria um erro:

// Flow irá retornar um erro corretamente
optionalLength(1);

Funções

Agora que cobrimos o básico, é hora de avançar um pouco mais. Já investigamos algumas funções na seção anterior, mas agora, vamos olhar alguns detalhes nos tipos de função. Em primeiro lugar, gostaríamos de digitar a entrada e saída de uma função, então vejamos como isso é feito.

let add = (a: number, b: number) : number => {
return a + a
}
add(2, 2);// Flow irá retornar um erro
add(2, 'foo');
const addResult : number = add(2, 2);

Tente executar o seguinte:

const addResultError : string = add(1, 2);

Você verá o seguinte erro:

const addResultError : string = add(1, 2);
^^^^^^^^^ number. This type is incompatible with
const addResultError : string = add(1, 2);
^^^^^^ string

Caso você queira usar declarações de função tradicionais em oposição às funções de seta, você pode escrever o exemplo anterior assim:

function addFunction(a: number, b: number) : number {
return a + a
}
addFunction(2, 2);// // Flow irá retornar um erro
addFunction(2, 'foo');
const addFunctionResult : number = addFunction(2, 2);

Para mais informações: https://flow.org/en/docs/types/functions

Arrays

Vamos continuar com arrays. Se você se lembra, no início desse artigo declaramos um arrays simples.

Há duas maneiras de declarar um array: Array ou Tipo []. Ou seja, esses dois são equivalentes:

const aArray : Array<number> = [1, 2, 3];
const aArrayShortHand : number[] = [1, 2, 3];

E se você quer ter um valor null dentro do array. É bem semelhante ao que vimos na seção de valores opcionais:

const aOptionalArray : Array<?number> = [1, null, 2, undefined];
const aOptionalArrayShortHand : (?number)[] = [1, null, 2, undefined];

E se quisermos ser mais específicos com a definição do array? Imagine o seguinte cenário:

Nós temos um array que contém exatamente três itens, uma string, um número e outra string: [‘foo’, 1, ‘bar’]. Nós chamamos essa estrutura de Tuplas.

const tupleA : [string, number, string]= ['foo', 1, 'bar'];// Flow retornará um erro
const tupleB : [string, number, number]= ['foo', 1, 'bar'];

Outro aspecto importante é que, uma vez que você tenha uma tupla definida, você não pode usar nenhum dos métodos existentes no objeto Array que mudem seus valores. Flow irá reclamar, por exemplo:

tupleA.push('foobar');

E você irá receber:

tupleA.push('foobar');
^^^^ property `push`. Property not found in
tupleA.push('foobar');
^^^^^^ $ReadOnlyArray

Então, uma vez que você tenha uma tupla definida, ela se torna somente para leitura (se os tipos declarados forem diferentes), em oposição a um array que pode ser mutável.

Outro breve exemplo:

const bArray : Array<number> = [1, 2, 3];bArray.push(4);// Flow retornará um erro
bArray.push('foo');
// Sem erro
bArray.push(4);

E por fim:

const tuple: [number, number] = [1, 2];// Sem erro
tuple.join(', ');
// Flow retornará um erro
tuple.push(3);
// ^ $ReadOnlyArray

Para mais informações:

Objetos

Vamos dar uma olhada em como o Flow funciona com objetos.

const aObject : Object = {id: 1, name: 'foo'};
const bObject : {id: number} = {id: 1, name: 'foo'};
const cObject : {id: number, name: string} = {id: 1, name: 'foo'};
// Flow retornará um erro
const dObject : {id: number, name: string, points: number} = {id: 1, name: 'foo'};

dObject retornará um erro, porque a chave points não está definida.

Queremos deixar points como um valor opcional. Já vimos como criar um valor opcional, então, vamos ver como alcançar o mesmo para uma propriedade de objeto.

const dRefinedObject : {id: number, name: string, points?: number} = {id: 1, name: 'foo'};

Ao declarar points ?: number, estamos dizendo que a chave points pode não ser definida.

Para tornar as coisas mais legíveis, você provavelmente irá definir um tipo de alias para a declaração desse objeto.

Isso é especialmente útil se você também planeja reutilizar uma definição de tipo.

type E = {id: number, name: string, points?: number};
const eObject : E = {id: 1, name: 'foo'};

Outra coisa importante a ser observada é que, ao trabalhar com objetos no Flow, existe um conceito de objetos selados e não selados.

Vamos ver os trechos seguintes:

const fObject = {
id: 1
}
// Flow retornará um erro
fObject.name = 'foo';

Portanto, o exemplo acima não funciona. Pois a definição é um objeto selado, ou seja, um objeto com propriedades definidas.

Um objeto não selado é definido como {}, um objeto que não possui propriedades. Agora, podemos adicionar uma propriedade do objeto, sem FlowType reclamar.

const gObject = {};
gObject.name = 'foo';

Outro aspecto importante ao trabalhar com objetos é que não precisamos ser exatos com nossa definição de tipo. Veja o próximo exemplo:

type F = {id: number, name: string};// Nenhum erro!
const fObject : F = {id: 1, name: 'foo', points: 100};

Mas e se quisermos ser exato? Podemos ser exatos envolvendo nossa definição dentro de {| ... |} dessa maneira:

type G = {|id: number, name: string|};// Flow retornará um erro
const gObject : G = {id: 1, name: 'foo', points: 100};
// Nenhum erro!
const gOtherObject : G = {id: 1, name: 'foo'};

Outra possibilidade é usar o utilitário $Exact<T>, disponível pelo Flow:

type H = $Exact<{id: number, name: string}>;// Flow retornará um erro
const hObject : H = {id: 1, name: 'foo', points: 100};

Uma abordagem comum em JavaScript é usar objetos como um mapa, novamente FlowType oferece uma maneira conveniente de digitar um mapa.

const aMap : {[number] : string} = {};aMap[1] = 'foo';
aMap['a'] = 'foo'; // Flow retornará um erro
aMap[1] = 1; // Flow retornará um erro
const otherMap : {[string]: number} = {};
otherMap['foo'] = 1;
otherMap[1] = 2; // Flow retornará um erro
otherMap['bar'] = 'foo'; // Flow retornará um erro

Podemos misturar declarações de propriedade com pares dinâmicos de chave/valor:

const mixedMap : {
id: number,
[string]: number
} = {
id: 1
}
mixedMap['foo'] = 1
mixedMap[1] = 2 // Flow retornará um erro
mixedMap['bar'] = 'foo' // Flow retornará um erro

Para mais informações: https://flow.org/en/docs/types/objects

Classes

Não resta muito a saber para poder escrever classes com o Flow. Você pode se referir à própria classe ao digitar uma instância de classe.

class Foo {
state = {val: 0}
update(val) {
this.state = {val}
}
getVal() {
return this.state.val
}
}
const foobar : Foo = new Foo();

E caso você tenha imaginado, métodos e propriedades da classe podem ser declarados! Vamos atualizar o exemplo anterior:

class Foo {
state : {val: number} = {val: 0}
update(val:number) : void {
this.state = {val}
}
getVal() : number {
return this.state.val
}
}
const foobar : Foo = new Foo()foobar.update(3)
foobar.update('foo') // Flow retornará um erro
const fooResult : number = foobar.getVal()
const fooResultError : string = foobar.getVal() // Flow retornará um erro

Para melhorar as coisas, vamos começar a falar de interface.

Em determinado cenário, nós temos uma class Bar, contendo uma propriedade state e um método update.

interface Updateable<T> {
state: {val: T};
update(a: T) : void
}
class Bar implements Updateable<boolean> {
state = {val: false}
constructor(val) {
this.state = {val}
}
update(val) {
this.state = {val}
}
getValue() {
return this.state.val
}
}
const barClass = new Bar(true);
const barClassResultOk : boolean = barClass.getValue();
// Flow retornará um erro
const barClassResultError : number = barClass.getValue();

Como você implementaria uma nova classe Foo que implementa Updateable?

Este é um pequeno exercício para o leitor interessado ;)

Para mais informações:

Generics

Agora estamos entrando em um território mais avançado. Até agora, descobrimos os fundamentos básicos necessários.

Vamos continuar com nosso exemplo anterior e adicionar Generics.

Por exemplo, nossa nova classe Foo também pode aceitar uma string ao invés de um número.

Queremos abstrair a definição de tipo neste caso:

class Foo<A> {
state : {val: A}
constructor(input: A) {
this.state = {val: input}
}
update(val:A) : void {
this.state = {val}
}
getVal() : A {
return this.state.val
}
}
const fooGenericNumber : Foo<number> = new Foo(1)
fooGenericNumber.update(2)
const fooGenericResult : number = fooGenericNumber.getVal()
const fooGenericString = new Foo('foo')
const fooGenericResultOther : string = fooGenericString.getVal()

Se você usar o REPL com o exemplo acima, você notará que tudo funciona corretamente.

Curiosamente, você nem precisa definir explicitamente um tipo para const fooGenericString = new Foo ('foo').

Flow saberá que nosso valor de retorno é uma string, como pode ser visto na linha a seguir.

Podemos fazer muito mais com generics, por exemplo, definir um alias de tipo ou funções.

Vamos ver alguns exemplos para ter uma melhor idéia das possibilidades.

type FooBar<A, B> = {
one: A,
two: B
}
const GenericAlias : FooBar<number, boolean> = {
one: 1,
two: false
}
// Flow retornará um erro
const GenericAliasError : FooBar<number, boolean> = {
one: 1,
two: 'foo'
}

E aqui, generics com funções:

const doubleIfPossible = <A>(a: A) : A => {
if (typeof a === 'string' || typeof a === 'number') {
return a + a;
}
return a;
}
const doubleIfPossibleResultOne : number = doubleIfPossible(2);
const doubleIfPossibleResultTwo : string = doubleIfPossible('foo');
// Flow retornará um erro
const doubleIfPossibleResultError : string = doubleIfPossible(true);

Há muito mais que você pode fazer com os generics.

Se você estiver interessado em saber mais, consulte a documentação oficial do Flow.

Para mais informações:

Por hora, isso é tudo! Este tutorial será atualizado de tempos em tempos.

Créditos

--

--