TypeScript: Declarações Const em expressões literais
Tags: ptbr, webdev, typescript, javascript
Published: 2020-01-22 07:45:20

Com o TypeScript 3.4, declarações `const` foram adicionadas à linguagem. Uma declaração `const` é um tipo especial de declaração de tipo no qual a palavra-chave `const` é usada ao invés de um nome de tipo. Neste artigo, explicarei como as declarações funcionam e onde podemos usá-las.

# Motivação para declaração const

Digamos que escrevemos a seguinte função `fetchJSON`. Ela aceita uma URL e um método de solicitação HTTP, usando a API Fetch do navegador para fazer uma solicitação GET ou POST para essa URL e desserializa a resposta como JSON:

```tsx
function fetchJSON(url: string, method: "GET" | "POST") {
  return fetch(url, { method })
    .then(response => response.json());
}
```

Podemos chamar essa função e passar um URL arbitrário para o parâmetro `url` e a string `"GET"` para o parâmetro `method`. Observe que estamos usando duas literais de string aqui:

```tsx
// OK, nenhum erro de tipo
fetchJSON("https://example.com/", "GET")
  .then(data => {
    // ...
  });
```

Para verificar se essa chamada da função tem o tipo correto, o TypeScript verifica os tipos de todos os argumentos passados na chamada da função em relação aos tipos de parâmetro definidos na declaração da função. Nesse caso, os tipos de ambos os argumentos são atribuíveis aos tipos de parâmetro e, portanto, essa chamada da função tem o tipo correto.

Vamos refatorar um pouco. A especificação HTTP define [vários métodos de solicitação](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) adicionais, como DELETE, HEAD, PUT e outros. Podemos definir um objeto de mapeamento no estilo `enum` chamado `HTTPRequestMethod` e listar os vários métodos de solicitação:

```tsx
const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE"
};
```

Agora podemos substituir a string literal `"GET"` em nossa chamada da função por `HTTPRequestMethod.GET`:

```tsx
fetchJSON("https://example.com/", HTTPRequestMethod.GET)
  .then(data => {
    // ...
  });
```

Mas agora, o TypeScript produz um erro de tipo! O verificador de tipos indica que o tipo de `HTTPRequestMethod.GET` não é atribuível ao tipo do parâmetro `method`:

```tsx
// Error: Argument of type 'string' is not assignable
// to parameter of type '"GET" | "POST"'.
```

Por isso ocorre? `HTTPRequestMethod.GET` retorna a string `"GET"`, o mesmo valor que passamos como argumento anteriormente. Qual é a diferença entre os tipos da propriedade `HTTPRequestMethod.GET` e a string literal `"GET"`? Para responder a essa pergunta, precisamos entender como os **tipos literais de string** funcionam e como o TypeScript realiza o **alargamento de tipos literais**.

# Tipos Literais de String

Vejamos o tipo do valor `"GET"` quando o atribuímos a uma variável declarada usando a palavra-chave `const`: 

```tsx
// Tipo: "GET"
const httpRequestMethod = "GET";
```

TypeScript infere o tipo `"GET"` para nossa variável `httpRequestMethod`. `"GET"` é chamado de tipo literal de string. Cada tipo literal descreve precisamente um valor, por exemplo, uma sequência específica, número, valor booleano ou membro de um enum. No nosso caso, estamos lidando com o valor da string `"GET"`, portanto nosso tipo literal é o tipo literal de string `"GET"`.

Observe que declaramos a variável `httpRequestMethod` usando a palavra-chave `const`. Portanto, sabemos que é impossível reatribuir a variável posteriormente; sempre manterá o valor `"GET"`. O TypeScript entende isso e infere automaticamente o tipo literal de seqüência de caracteres `"GET"` para representar essas informações no sistema de tipos.

# Alargamento de Tipo Literal

Vamos agora ver o que acontece se usarmos a palavra-chave `let` (ao invés de `const`) para declarar a variável `httpRequestMethod`: 

```tsx
// Tipo: string
let httpRequestMethod = "GET";
```

O TypeScript agora executa o que é conhecido como [alargamento de tipo literal](https://mariusschulz.com/blog/literal-type-widening-in-typescript). A variável `httpRequestMethod` é inferida como tendo o tipo `string`. Estamos inicializando `httpRequestMethod` com a string `"GET"`, mas como a variável é declarada usando a palavra-chave `let`, podemos atribuir outro valor a ela posteriormente:

```tsx
// Tipo: string
let httpRequestMethod = "GET";

// OK, nenhum erro de tipo
httpRequestMethod = "POST";
```

A atribuição posterior do valor `"POST"` está correta, pois `httpRequestMethod` possui o tipo `string`. O TypeScript inferiu o tipo `string` porque provavelmente queremos alterar o valor de uma variável declarada usando a palavra-chave `let` mais tarde. Se não reatribuirmos a variável, deveríamos ter usado a palavra-chave `const`.

Vamos agora olhar para o nosso objeto em estilo `enum`:

```tsx
const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE"
};
```

Que tipo `HTTPRequestMethod.GET` tem? Vamos descobrir:

```tsx
// Tipo: string
const httpRequestMethod = HTTPRequestMethod.GET;
```

TypeScript infere o tipo string para nossa variável httpRequestMethod. Isso ocorre porque estamos inicializando a variável com o valor HTTPRequestMethod.GET (que possui o tipo string), portanto, o tipo string é inferido.

Então, por que HTTPRequestMethod.GET tem tipo string e não tipo "GET"? Estamos inicializando a propriedade GET com a string literal "GET", e o objeto HTTPRequestMethod é definido usando a palavra-chave const . O tipo resultante não deve ser o tipo literal de string "GET"?

O motivo pelo qual o TypeScript inferi o tipo string em HTTPRequestMethod.GET (e todas as outras propriedades) é que poderíamos atribuir outro valor a qualquer uma das propriedades posteriormente. Para nós, esse objeto com seus nomes de propriedade em "ALL_UPPERCASE" parece uma enumeração que define constantes de seqüência de caracteres que não serão alteradas ao longo do tempo. No entanto, para o TypeScript, esse é apenas um objeto comum, com algumas propriedades que são inicializadas com valores em sequência.

O exemplo a seguir torna um pouco mais óbvio por que o TypeScript não deve inferir um tipo literal de string para propriedades do objeto inicializadas com um literal de string:

```tsx
// Tipo: { name: string, jobTitle: string }
const person = {
  name: "Marius Schulz",
  jobTitle: "Software Engineer"
};

// OK, nenhum erro de tipo
person.jobTitle = "Front End Engineer";
```

Se a propriedade jobTitle for inferida como do tipo "Software Engineer", seria um erro de tipo se jobTitle atribuir qualquer sequência diferente de "Software Engineer" posteriormente. Nossa atribuição de "Front End Engineer" não seria correta. As propriedades do objeto são mutáveis por padrão, portanto, não queremos que o TypeScript deduza um tipo que nos impeça de executar uma mutação perfeitamente válida.

Então, como fazemos o uso da nossa propriedade HTTPRequestMethod.GET na verificação de tipo na chamada da função? Precisamos entender primeiro os **tipos literais não ampliadores**.

# Tipos Literais sem alargamento

O TypeScript possui um tipo especial de tipo literal, conhecido como ["tipo literal sem alargamento"](https://mariusschulz.com/blog/literal-type-widening-in-typescript#non-widening-literal-types). Como o nome sugere, os tipos literais não são ampliados para um tipo mais genérico. Por exemplo, o tipo literal de seqüência de caracteres que não requer alargamento `"GET"`, não será ampliado para `string`, nos casos em que normalmente ocorre o alargamento de tipo.

Podemos fazer com que as propriedades do nosso objeto HTTPRequestMethod recebam um tipo literal que não possa ser ampliado usando uma asserção de tipo do valor literal da string, correspondente a cada valor da propriedade:

```tsx
const HTTPRequestMethod = {
  CONNECT: "CONNECT" as "CONNECT",
  DELETE: "DELETE" as "DELETE",
  GET: "GET" as "GET",
  HEAD: "HEAD" as "HEAD",
  OPTIONS: "OPTIONS" as "OPTIONS",
  PATCH: "PATCH" as "PATCH",
  POST: "POST" as "POST",
  PUT: "PUT" as "PUT",
  TRACE: "TRACE" as "TRACE"
};
```

Agora, vamos verificar o tipo de HTTPRequestMethod.GET novamente:

```tsx
// Tipo: "GET"
const httpRequestMethod = HTTPRequestMethod.GET;
```

E, de fato, agora a variável httpRequestMethod tem o tipo "GET" ao invés do tipo string. O tipo de HTTPRequestMethod.GET (que é "GET") é atribuível ao tipo do parâmetro do `method` (que é "GET" | "POST") e, portanto, a chamada da função `fetchJSON` agora verifica corretamente seus tipo:

```tsx
// OK, nenhum erro de tipo
fetchJSON("https://example.com/", HTTPRequestMethod.GET)
  .then(data => {
    // ...
  });
```

É uma ótima notícia, mas dê uma olhada no número de asserções de tipo que tivemos que escrever para chegar a esse ponto. Isso é muito confuso! Agora, cada par de chave / valor contém o nome do método de solicitação HTTP três vezes. Podemos simplificar essa definição? Usando o recurso de afirmações `const` do TypeScript, certamente podemos!

# Declarações Const em expressões literais

Nossa variável HTTPRequestMethod é inicializada com uma expressão literal que é um objeto literal com várias propriedades, todas inicializadas com literais de string. A partir do TypeScript 3.4, podemos aplicar uma declaração `const` a uma expressão literal:

```tsx
const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE"
} as const;
```

Uma declaração `const` é uma asserção de tipo especial que usa a palavra-chave `const` ao invés de um nome de tipo específico. Usar uma declaração `const` em uma expressão literal tem os seguintes efeitos:

1. Nenhum tipo literal na expressão literal será [ampliado](https://mariusschulz.com/blog/literal-type-widening-in-typescript).
2. Literais de objeto obterão [propriedades `readonly`](https://mariusschulz.com/blog/read-only-properties-in-typescript).
3. Literais de arrays se tornarão [`tuplas readonly`](https://mariusschulz.com/blog/read-only-array-and-tuple-types-in-typescript).

Com a declaração const, a definição acima de HTTPRequestMethod é equivalente ao seguinte:

```tsx
const HTTPRequestMethod: {
  readonly CONNECT: "CONNECT";
  readonly DELETE: "DELETE";
  readonly GET: "GET";
  readonly HEAD: "HEAD";
  readonly OPTIONS: "OPTIONS";
  readonly PATCH: "PATCH";
  readonly POST: "POST";
  readonly PUT: "PUT";
  readonly TRACE: "TRACE";
} = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE"
};
```

Não gostaríamos de escrever essa definição manualmente. É bem detalhada e contém muita repetição; observe que todo método de solicitação HTTP está escrito quatro vezes. A afirmação `const` em `as const`, por outro lado, é muito sucinta e o único pedaço da sintaxe específica do TypeScript em todo o exemplo.

Além disso, observe que todas as propriedades agora são digitadas como readonly. Se [tentarmos atribuir um valor a uma propriedade somente leitura](https://mariusschulz.com/blog/read-only-properties-in-typescript), o TypeScript produzirá um erro de tipo:

```tsx
// Error: Cannot assign to 'GET'
HTTPRequestMethod.GET = "...";

// Pois a propriedade é read-only.
```

Com a afirmação const, demos ao nosso objeto HTTPRequestMethod características de enumeração. Mas e como fica os enums em TypeScript?

# Usando enums em TypeScript

Outra solução possível seria usar [TypeScript Enum](https://www.typescriptlang.org/docs/handbook/enums.html) ao invés de um objeto literal simples. Poderíamos ter definido HTTPRequestMethod usando a palavra-chave enum assim:

```tsx
enum HTTPRequestMethod {
  CONNECT = "CONNECT",
  DELETE = "DELETE",
  GET = "GET",
  HEAD = "HEAD",
  OPTIONS = "OPTIONS",
  PATCH = "PATCH",
  POST = "POST",
  PUT = "PUT",
  TRACE = "TRACE"
}
```

O Enum em TypeScript são feitos para descrever constantes nomeadas, e é por isso que seus membros são sempre de somente leitura. Os membros de uma [enumeração de string](https://mariusschulz.com/blog/string-enums-in-typescript) têm um tipo literal de string:

```tsx
// Tipo: "GET"
const httpRequestMethod = HTTPRequestMethod.GET;
```
Isso significa que nossa chamada de função irá verificar o tipo quando passarmos HTTPRequestMethod.GET como argumento para o parâmetro method:

```tsx
// OK, nenhum erro de tipo
fetchJSON("https://example.com/", HTTPRequestMethod.GET)
  .then(data => {
    // ...
  });
```

No entanto, alguns desenvolvedores não gostam de usar enumerações TypeScript em seu código porque a sintaxe da enum não é JavaScript válida por si só. O compilador TypeScript emitirá o seguinte código JavaScript para nosso enum HTTPRequestMethod definido acima:

```tsx
var HTTPRequestMethod;
(function (HTTPRequestMethod) {
    HTTPRequestMethod["CONNECT"] = "CONNECT";
    HTTPRequestMethod["DELETE"] = "DELETE";
    HTTPRequestMethod["GET"] = "GET";
    HTTPRequestMethod["HEAD"] = "HEAD";
    HTTPRequestMethod["OPTIONS"] = "OPTIONS";
    HTTPRequestMethod["PATCH"] = "PATCH";
    HTTPRequestMethod["POST"] = "POST";
    HTTPRequestMethod["PUT"] = "PUT";
    HTTPRequestMethod["TRACE"] = "TRACE";
})(HTTPRequestMethod || (HTTPRequestMethod = {}));
```

Depende inteiramente de você decidir se deseja usar literais de objeto simples ou enumerações TypeScript. Se você deseja permanecer o mais próximo possível do JavaScript e usar apenas o TypeScript para anotações de tipo, você pode usar literais simples de objeto e declarações de const. Se você não se importa em usar sintaxe não padrão para definir enumerações e gosta da comodidade, as enumerações TypeScript podem ser uma boa opção.

# Declarações const para outros tipos

Você pode aplicar uma declaração const:

- literais de string
- literais numéricos
- literais booleanos
- literais de matriz
- literais de objeto

Por exemplo, você pode definir uma variável ORIGIN descrevendo a origem no espaço bidimensional como este:

```tsx
const ORIGIN = {
  x: 0,
  y: 0
} as const;
```

Isso é equivalente (e muito mais sucinto que) à seguinte declaração:

```tsx
const ORIGIN: {
  readonly x: 0;
  readonly y: 0;
} = {
  x: 0,
  y: 0
};
```

Como alternativa, você poderia modelar a representação de um ponto como uma tupla das coordenadas X e Y:

```tsx
// Tipo: readonly [0, 0]
const ORIGIN = [0, 0] as const;
```

Por causa da afirmação const, ORIGIN é digitado como `readonly [0, 0]`. Sem a afirmação, ORIGIN teria sido inferido como tendo o tipo `number[]`:

```tsx
// Tipo: number[]
const ORIGIN = [0, 0];
```

_Esse artigo faz parte da série [TypeScript Evolution](https://mariusschulz.com/blog/series/typescript-evolution)_

# Créditos

- [Const Assertions in Literal Expressions in TypeScript](https://mariusschulz.com/blog/const-assertions-in-literal-expressions-in-typescript), escrito originalmente por [Marius Schulz](https://twitter.com/mariusschulz).