Flow: Fundamentos para Desenvolvedores JavaScript
Tudo o que você precisa saber para começar com o FlowType.
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 withconst 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 erroconst 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 erroconst 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:
- Classes: https://flow.org/en/docs/types/classes
- Interfaces: https://flow.org/en/docs/types/interfaces
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:
- Generics: https://flow.org/en/docs/types/generics/
Por hora, isso é tudo! Este tutorial será atualizado de tempos em tempos.
Créditos
- Flow Fundamentals For JavaScript Developers, escrito originalmente por A. Sharif