let e constsão dois conceitos relativamente novos para declarações de variáveis em JavaScript.
Como mencionamos anteriormente, let é similar a var em alguns aspectos, mas evita que alguns usuários caiam em momentos “te peguei” em JavaScript.
const é uma ampliação de let no qual previne reatribuições a uma variável.
Com TypeScript sendo uma extensão de JavaScript, a linguagem naturalmente suporta let e const.
Aqui iremos nos aprofundar nessas novas declarações e porque elas tem melhor preferência do que var.
Se você vem usando JavaScript descuidadamente, a próxima sessão pode ser uma boa maneira de refrescar sua memória.
Se você está intimamente familiarizado com todas as peculiaridades de declarações var em JavaScript, talvéz você ache mais fácil pular essa sessão.
declarações var
Tradicionalmente, declarar uma variável em JavaScript sempre foi feito usando a palavra chave var.
tsvar a = 10;
Como você ja deve ter descoberto, apenas declaramos a variável chamada a com o valor 10.
Podemos também declarar uma variável dentro de uma função:
tsfunction f() {var mensagem = "Olá, mundo!";return mensagem;}
e também podemos acessar essas mesmas variáveis através de outras funções:
tsfunction f() {var a = 10;return function g() {var b = a + 1;return b;};}var g = f();g(); // retorna '11'
No exemplo acima, g capturou a variável a declarada em f.
Em qualquer ponto que g for chamada, o valor de a será atrelado com o valor de a em f.
Mesmo se g for chamada quando f tiver terminado de rodar, ela será capaz de acessar e modificar a.
tsfunction f() {var a = 1;a = 2;var b = g();a = 3;return b;function g() {return a;}}f(); // retorna '2'
Regras de escopo
Declarações var possuem regras de escopo estranhas para aqueles acostumados com outras linguagens.
Veja o exemplo a seguir:
tsfunction f(deveriaInicializar: boolean) {if (deveriaInicializar) {var x = 10;}return x;}f(true); // retorna '10'f(false); // retorna 'undefined'
Talvez alguns leitores precisem dar uma segunda olhada nesse exemplo.
A variável x foi declarada dentro do bloco if, e mesmo assim fomos capazes de acessa-la de fora daquele bloco.
Isso porque declarações var são acessíveis em qualquer lugar dentro da função, módulo, namespace ou escopo global em que estão contidas - o que iremos ver mais à frente - independente do bloco contido.
Algumas pessoas chamam isso escopo-var ou escopo de função.
Parâmetros também tem escopo de função.
Essas regras de escopo podem causar muitos tipos de erros. Um problema que elas deixam exacerbado é o fato de que não é um erro declarar a mesma variável várias vezes:
tsfunction sumaMatriz(matriz: number[][]) {var soma = 0;for (var i = 0; i < matriz.length; i++) {var linhaAtual = matriz[i];for (var i = 0; i < linhaAtual.length; i++) {soma += linhaAtual[i];}}return soma;}
Talvez tenha sido fácil para alguns desenvolvedores JavaScript experientes identificar, mas o loop for interno acidentalmente sobrescreve a variável i porque i faz referência para a mesma variável com escopo de função.
Como desenvolvedores experientes já sabem, tipos similares de bugs escorregam pelo code review e podem ser uma fonte interminável de frustração.
Captura de peculiaridades em variáveis
Gaste alguns segundos para adivinhar qual é a saída do seguinte trecho de código:
tsfor (var i = 0; i < 10; i++) {setTimeout(function () {console.log(i);}, 100 * i);}
Para aqueles sem familiaridade, setTimeout irá tentar executar a função após um certo numero de milissegundos (esperando, entretanto, qualquer outra coisa parar de rodar).
Preparado? Dê uma olhada:
10101010101010101010
Muitos desenvolvedores JavaScript estão intimamente familiarizados com esse comportamento, mas se você está surpreso, certamente não esta sozinho. A maioria das pessoas espera que a saída seja:
0123456789
Lembra o que mencionamos anteriormente sobre captura de variáveis?
Toda expressão função que passamos para setTimeout na verdade faz referencia para i do mesmo escopo.
Vamos parar um pouco para entender o que isso significa.
setTimeout irá executar a função após determinado numero de milissegundos, mas apenas após o loop for ter parado de executar;
Quando o loop for parar sua execução, o valor de i é 10.
Então cada vez que a função for chamada irá imprimir 10!
Uma forma comum de contornar o caso é usar um IIFE - uma Immediately Invoked Function Expression (Expressão de Invocação Imediata) - para capturar i a cada iteração:
tsfor (var i = 0; i < 10; i++) {// captura o estado atual de 'i'// ao invocar a função com seu valor atual(function (i) {setTimeout(function () {console.log(i);}, 100 * i);})(i);}
Esse padrão de aparência estranha é, na verdade, bem comum.
O i na lista de parâmetros, na verdade, oculta o i declarado no loop for, mas como demos o mesmo nome para ambos, não temos de modificar muito o corpo do loop.
Declarações let
Até agora você descobriu que var tem alguns problemas, o que é precisamente o porque afirmações let foram introduzidas.
Independentemente da palavra chave usada, afirmações let são escritas da mesma forma que afirmações var.
tslet ola = "Olá!";
A principal diferença não está na sintaxe, mas na semântica, na qual iremos nos aprofundar agora.
Escopo de bloco
Quando uma variável é declarada usando let, usa-se o que alguns chamam de escopo léxico ou escopo de bloco.
Diferente de variáveis declaradas com var no qual o escopo permeia suas funções, variáveis com escopo de bloco não são visíveis de fora de seus blocos mais próximos ou loop-for.
tsfunction f(input: boolean) {let a = 100;if (input) {// Ainda é ok referenciar 'a'let b = a + 1;return b;}// Erro: 'b' não existe aquireturn b;}
Aqui, temos duas variáveis locais a e b.
O escopo de a é limitado ao corpo de f em quanto o escopo de b é limitado ao bloco if na qual está contida.
Variáveis declaradas em uma cláusula catch também possuem regras similares de escopo.
tstry {throw "ah não!";} catch (e) {console.log("Eh bem.");}// Erro: 'e' não existe aquiconsole.log(e);
Outra propriedade de variáveis com escopo de bloco é que elas não podem ser lidas ou escritas antes de serem declaradas.
Enquanto essas variáveis estão “presentes” através de seu escopo, todas apontam para cima até que sua declaração seja parte de sua zona morta temporal.
Isso é apenas uma forma sofisticada de dizer que você não pode acessa-las antes da afirmação let e por sorte o TypeScript fará com que você saiba disso.
tsa++; // ilegal usar `a` antes de ser declarada;let a;
Algo a se notar é que você ainda pode capturar uma variável de escopo antes dela ser declarada. O único problema é que é ilegal chamar essa função antes da declaração. Se tivermos como foco ES2015, um ambiente de execução moderno irá lançar um erro; entretanto, atualmente TypeScript é permissívo e não irá reportar isso com um erro.
tsfunction foo() {// ok capturar 'a'return a;}// chamada ilegal 'foo' antes de 'a' ser declarada// ambiente de execução deveria lançar um erro aquifoo();let a;
Para mais informações sobre zonas mortas temporais, veja conteúdo relevante no Mozilla Developer Network.
Re-declarações e Shadowing
Com declarações var, mencionamos que não importa quantas vezes você declara suas variáveis; você tem apenas uma.
tsfunction f(x) {var x;var x;if (true) {var x;}}
No exemplo acima, todas as declarações de x na verdade fazem referencia para o mesmo x, e isso é perfeitamente válido.
Isso acaba sendo fonte de erros com frequência.
Felizmente, declarações let não são tão permissíveis.
tslet x = 10;let x = 20; // erro: não pode redeclarar 'x' no mesmo escopo
Ambas as variáveis não precisam necessáriamente ter escopo de bloco para o TypeScript nos falar que existe um problema.
tsfunction f(x) {let x = 100; // erro: interfere com a declaração de parâmetros}function g() {let x = 100;var x = 100; // erro: não pode haver ambas declarações de 'x'}
Isso não quer dizer que uma variável de bloco nunca pode ser declarada com uma variável com escopo de função. A variável de escopo de bloco precisa apenas ser declarada dentro de um bloco diferente distinguível.
tsfunction f(condicao, x) {if (condicao) {let x = 100;return x;}return x;}f(false, 0); // retorna '0'f(true, 0); // retorna '100'
O ato de introduzir um novo nome em um escopo mais encadeado é chamado shadowing.
Isso é meio que uma faca de dois gúmes pois pode introduzir certos bugs por sí só no evento de shadowing acidental, em quanto também pode prevenir certos bugs.
Por exemplo, imagine que tenhamos escrito nossa função anterior somaMatriz usando variáveis let.
tsfunction somaMatriz(matriz: number[][]) {let soma = 0;for (let i = 0; i < matriz.length; i++) {var linhaAtual = matriz[i];for (let i = 0; i < linhaAtual.length; i++) {soma += linhaAtual[i];}}return soma;}
Essa versão do loop irá, na verdade, fazer a soma corretamente porque o i do loop interior espelha o i do loop exterior.
O shadowing deveria normalmente ser evitado com o intuito de escrever código mas claro.
Embora existam alguns cenários onde ele pode se encaixar de forma vantajosa, você deve julgar a melhor forma de usa-lo.
Captura de variáveis de escopo de bloco
Quando abordamos pela primeira vez a idéia de captura de variáveis com declarações var, abordamos de forma breve como variáveis agem quando capturadas.
Para dar uma melhor visão sobre isso, cada vez que um escopo é rodado, ele cria um “ambiente” de variáveis.
Esse ambiente e suas variáveis capturadas podem existir mesmo após tudo em seu escopo tiver terminado de ser executado.
tsfunction aCidadeQueSempreDorme() {let getCidade;if (true) {let cidade = "Seattle";getCidade = function () {return cidade;};}return getCidade();}
Porque nós capturamos cidade de dentro de seu ambiente, ainda somos capazes de acessa-la a pesar do fato de que o bloco if terminou sua execução.
Lembre que com nosso exemplo anterior setTimeout, adicionamos a necessidade de usar um IIFE para capturar o estado de uma variável para cada iteração do loop for.
Na prática, o que estávamos fazendo era criar uma nova variável de ambiente para nossas variáveis capturadas.
Isso foi um pouco doloroso, mas felizmente, você nunca terá de fazer isso em TypeScript de novo.
Declarações let tem um comportamento drasticamente diferentes quando declaradas como parte de um loop.
Além de apenas introduzir um novo ambiente ao próprio loop, essas declarações meio que criam um novo escopo por iteração.
Como isso é o que estamos fazendo de qualquer forma com nosso IIFE, nós podemos mudar nosso exemplo antigo setTimeout para usar apenas uma declaração let.
tsfor (let i = 0; i < 10; i++) {setTimeout(function () {console.log(i);}, 100 * i);}
como esperado, isso irá imprimir
0123456789
Declarações const
Declarações const são outra forma de declarar variáveis.
tsconst numVidasParaPorGato = 9;
Elas são como declarações let mas, como seu nome indica, seu valor não pode ser alterado uma vez que o mesmo é delimitado.
Em outras palavras, elas tem a mesma regra de escopo de let, mas você não pode reatribui-las.
Isso não deveria ser confundido com a idéia de que os valores nos quais elas fazem referência são imutáveis.
tsconst numVidasParaPorGato = 9;const gatinho = {nome: "Aurora",numVidas: numVidasParaPorGato,};// Errogatinho = {name: "Danielle",numVidas: numVidasParaPorGato,};// tudo "okay"gatinho.nome = "Rory";gatinho.nome = "Kitty";gatinho.nome = "Cat";gatinho.numVidas--;
A não ser que você tome medidas específicas para evitar, o estado interior de uma variável const ainda é modificável.
Felizmente, TypeScript te permite especificar que membros de um objeto são readonly.
O capítulo sobre Interfaces tem mais detalhes.
let vs. const
Dado que temos dois tipos de declarações com semântica de escopo similares, é natural nos perguntarmos qual usar. Como a maioria das perguntas amplas, a resposta é: depende.
Aplicando o princípio do menos privilegiado, todas declarações além daquelas que você planeja modificar deveriam usar const.
A justificativa é que, se uma variável não precisa ser escrita, outros trabalhando na mesma base de dados não deveriam ser possibilitados de escrever automaticamente no objeto, e precisarão considerar quando eles realmente precisam reatribuir para a variável.
Usando const também faz o código mais previsível quando se está raciocinando sobre o fluxo de dados.
Use seu melhor julgamento, e se aplicável, consulte o assunto com o resto de seu time.
A maioria desse manual usa declarações let.
Desestruturação
Outra funcionalidade do ECMAScript 2015 que o TypeScript tem é desestruturação.
Para uma referência completa, veja o artigo na Mozilla Developer Network. Nessa sessão, daremos uma visão geral breve.
Desestruturação de Array
A forma mais simples de desestruturação são atribuições de desestruturação de arrays:
tslet entrada = [1, 2];let [primeiro, segundo] = entrada;console.log(primeiro); // retorna saída 1console.log(segundo); // retorna saída 2
Isso cria duas novas variáveis chamadas primeiro e segundo.
Isso é equivalente a usar indexação, mas é muito mais conveniente:
tsprimeiro = entrada[0];segundo = entrada[1];
Desestruturação também funciona com variáveis ja declaradas:
ts// variáveis de troca[primeiro, segundo] = [segundo, primeiro];
E com parâmetros para uma função:
tsfunction f([primeiro, segundo]: [number, number]) {console.log(primeiro);console.log(segundo);}f([1, 2]);
Você pode criar uma variável para os itens restantes da lista usando a sintaxe ...:
tslet [primeiro, ...restante] = [1, 2, 3, 4];console.log(primeiro); // retorna saída 1console.log(restante); // retorna saída [ 2, 3, 4 ]
Claro, como isso é JavaScript, você pode apenas ignorar elementos finais que não se importa:
tslet [primeiro] = [1, 2, 3, 4];console.log(primeiro); // retorna saída 1
Ou outros elementos:
tslet [, segundo, , quarto] = [1, 2, 3, 4];console.log(segundo); // retorna saída 2console.log(quarto); // retorna saída 4
Desestruturação de Tupla
Tuplas podem ser desestruturadas como arrays; as variáveis de desestruturação pegam os tipos dos elementos tupla correspondentes:
tslet tupla: [number, string, boolean] = [7, "olá", true];let [a, b, c] = tupla; // a: number, b: string, c: boolean
É um erro desestruturar a tupla além do limite de seus elementos:
tslet [a, b, c, d] = tupla; // Erro, sem elementos no index 3
Como nos arrays, você pode desestruturar o resto da tupla com ..., para obter uma tupla mais curta:
tslet [a, ...bc] = tupla; // bc: [string, boolean]let [a, b, c, ...d] = tupla; // d: [], a tupla vazia
Ou ignorar elementos finais, ou outros elementos:
tslet [a] = tupla; // a: numberlet [, b] = tupla; // b: string
Desestruturação de Objetos
Você também pode desestruturar objetos:
tslet o = {a: "foo",b: 12,c: "bar",};let { a, b } = o;
Isso cria novas variáveis a e b a partir de o.a e o.b.
Perceba que você pode pular c se você não o quiser.
Como desestruturação de arrays, você pode ter atribuições sem declarações:
ts({ a, b } = { a: "baz", b: 101 });
Perceba que tivemos que cercar esse elemento com parêntesis.
JavaScript normalmente analisa um { como o começo do bloco.
Você pode criar uma variável para o restante dos itens em um objeto usando a sintaxe ...:
tslet { a, ...atravessando } = o;let total = atravessando.b + atravessando.c.length;
Renomeação de propriedades
Você também pode dar nomes diferentes para propriedades:
tslet { a: novoNome1, b: novoNome2 } = o;
Aqui a sintaxe começa a ficar confusa.
Você pode ler a: novoNome1 como ”a sendo novoNome1”.
A direção é esquerda para direita, como se você tivesse escrito:
tslet novoNome1 = o.a;let novoNome2 = o.b;
De forma confusa, os dois pontos não indicam o tipo. O tipo, se você o especificar, ainda precisa ser escrito após toda desestruturação:
tslet { a, b }: { a: string; b: number } = o;
Valores padrão
Valores padrão te permitem especificar um valor caso a propriedade seja undefined:
tsfunction mantenhaObjetoInteiro(objetoInteiro: { a: string; b?: number }) {let { a, b = 1001 } = objetoInteiro;}
Nesse exemplo o b? indica que b é opcional, então ele pode ser undefined.
mantenhaObjetoInteiro agora tem uma variável para objetoInteiro assim como as propriedades a e b, mesmo se b for undefined.
Declaração de Funções
Desestruturação também funciona em declarações de funções. Para casos simples é bem descomplicado:
tstype C = { a: string; b?: number };function f({ a, b }: C): void {// ...}
Mas especificar valores padrões é mais comum para parâmetros, e ter valores padrão de forma correta com desestruturação pode ser complicado. Primeiro, você precisa lembrar de por a padronização antes do valor padrão.
tsfunction f({ a = "", b = 0 } = {}): void {// ...}f();
O código acima é um exemplo de inferência de tipo, explicado anteriormente nesse manual.
Então, você precisa lembrar de prover um valor padrão para parâmetros opcionais na propriedade desestruturada ao invés do inicializador principal.
Lembre que C foi definida com b opcional:
tsfunction f({ a, b = 0 } = { a: "" }): void {// ...}f({ a: "sim" }); // ok, padrão b = 0f(); // ok, padrão para { a: "" }, no qual então define padrão b = 0f({}); // erro, 'a' é requerido se você fornecer um argumento
Use desestruturação com cuidado. Como os exemplos anteriores demonstram, qualquer coisa a mais do que desestruturação simples é confuso. Isso é especialmente verdade com desestruturação em encadeamentos profundos, que fica realmente difícil de entender mesmo sem empilhamento, renomeação, valores padrão e anotações de tipo. Tente manter expressões de desestruturação pequenas e simples. Você pode sempre escrever as atribuições que a desestruturação geraria.
Propagação
O operador de propagação é o oporto do de desestruturação. Ele te permite propagar um array para outro array, ou um objeto para outro objeto. Por exemplo:
tslet primeiro = [1, 2];let segundo = [3, 4];let ambosMais = [0, ...primeiro, ...segundo, 5];
Isso dá a ambosMais o valor [0, 1, 2, 3, 4, 5].
Propagação cria uma cópia rasa de primeiro e segundo.
Eles não são modificados pela propagação.
Você também pode propagar objetos:
tslet padroes = { comida: "apimentada", preco: "$$", ambiente: "barulhento" };let busca = { ...padroes, comida: "rica" };
Agora busca é { comida: "rica", preco: "$$", ambiente: "barulhento" }.
Propagação de objetos é mais complexo do que propagação de array.
Como propagação de array, procede-se da esquerda para direita, mas o resultado ainda é um objeto.
Isso significa que propriedades que vem depois no objeto propagado sobrescrevem propriedades que vieram anteriormente.
Então se modificarmos o exemplo anterior para propagar no final:
tslet padroes = { comida: "apimentada", preco: "$$", ambiente: "barulhento" };let busca = { comida: "rica", ...padroes };
Então a propriedade comida em padroes sobrescreve comida: "rica", o que não é o que queremos nesse caso.
Propagação de objetos também possui outros limites surpreendentes. Primeiro, ele apenas inclui as próprias propriedades enumeradas de um objeto. Basicamente, isso significa que você perde métodos quando propaga instancias de um objeto:
tsclass C {p = 12;m() {}}let c = new C();let clone = { ...c };clone.p; // okclone.m(); // erro!
Segundo, o compilador TypeScript não permite propagação do tipo dos parâmetros para funções genéricas. Essa funcionalidade é esperada em versões futuras da linguagem.