DEV Community

Cover image for TypeScript: Declarações Const em expressões literais
Eduardo Rabelo
Eduardo Rabelo

Posted on

TypeScript: Declarações Const em expressões literais

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:

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:

// 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 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:

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:

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:

// 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:

// 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:

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

O TypeScript agora executa o que é conhecido como alargamento de tipo literal. 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:

// 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:

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:

// 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:

// 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". 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:

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:

// 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:

// 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:

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.
  2. Literais de objeto obterão propriedades readonly.
  3. Literais de arrays se tornarão tuplas readonly.

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

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, o TypeScript produzirá um erro de tipo:

// 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 ao invés de um objeto literal simples. Poderíamos ter definido HTTPRequestMethod usando a palavra-chave enum assim:

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 têm um tipo literal de string:

// 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:

// 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:

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:

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

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

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:

// 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[]:

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

Esse artigo faz parte da série TypeScript Evolution

Créditos

Top comments (0)