Tipos primitivos
por Sutton AshfordEntenda o conceito
Um tipo primitivo é um valor simples, que não é objeto. O JavaScript, e portanto o TypeScript, tem sete: string, number, boolean, bigint, symbol, null e undefined. Os três primeiros aparecem em quase todo programa; bigint guarda inteiros de qualquer tamanho (escritos com n no fim, como 10n) e symbol cria identificadores que nunca se repetem.
O number é um só tipo para inteiros e decimais, no formato de ponto flutuante de 64 bits. Isso tem dois limites práticos: alguns decimais não são exatos (0.1 + 0.2 dá 0.30000000000000004) e inteiros só são exatos até Number.MAX_SAFE_INTEGER. Acima disso, use bigint; para dinheiro, trabalhe com centavos inteiros. Dividir por zero não lança erro: o resultado é Infinity.
No TypeScript, cada primitivo também tem tipos literais: 'A', 14 e true são tipos que aceitam só aquele valor. Uma const recebe o tipo literal do seu valor, enquanto um let recebe o primitivo inteiro. Os template literals (texto entre crases com ${}) montam strings com expressões e várias linhas, e os tipos template literal descrevem o formato de um texto, como `ROM-${number}`.
- São sete primitivos:
string,number,boolean,bigint,symbol,nulleundefined; o tipo em minúsculas é o certo (string, nãoString). numbernão é exato para todos os decimais: compare com tolerância e arredonde só na exibição; para dinheiro, use centavos inteiros.- Inteiros acima de
Number.MAX_SAFE_INTEGERperdem precisão emnumber; usebigint, que nunca se mistura comnumbersem conversão. const x = 14tem o tipo literal14;let x = 14tem o tiponumber, porque pode mudar.- Um
symbolé sempre único, mesmo com a mesma descrição, e não vira texto sozinho: useString(s)ous.description.
Do conceito à prática
3 exemplos para ver a ideia funcionando.
01Os primitivos na ficha da receita
// Ficha de uma receita: um valor de cada tipo primitivo, com anotação explícita.
const nome: string = 'Pão de queijo';
const porcoes: number = 24;
const minutosNoForno: number = 25.5; // number guarda inteiros e decimais
const semGluten: boolean = true;
// bigint: inteiro de qualquer tamanho, escrito com n no fim.
const visualizacoes: bigint = 12_500_000_000n;
// symbol: um identificador único; a descrição serve só para depuração.
const idInterno: symbol = Symbol('receita');
console.log(`${nome} (${typeof nome})`);
console.log(`${porcoes} porções, ${minutosNoForno} min (${typeof porcoes})`);
console.log(`Sem glúten? ${semGluten} (${typeof semGluten})`);
console.log(`${visualizacoes} visualizações (${typeof visualizacoes})`);
// Symbol não vira texto sozinho: use .description ou String().
console.log(`${String(idInterno)} (${typeof idInterno})`);
// bigint só opera com bigint: o 1 também precisa do n.
const depoisDeHoje: bigint = visualizacoes + 1n;
console.log(`Com mais uma visita: ${depoisDeHoje}`);
// const visitas = visualizacoes + 1; // erro: não mistura bigint e number
// console.log(`${idInterno}`); // erro: symbol não converte para string
Cada constante tem um tipo primitivo diferente, anotado de forma explícita, e o typeof confirma o tipo enquanto o programa roda. O number serve tanto para 24 quanto para 25.5: não existe um tipo separado para inteiros. O bigint é escrito com n no fim e o _ entre os dígitos só facilita a leitura.
As linhas comentadas mostram o que o tsc recusa: somar bigint com number (é preciso escrever 1n) e colocar um symbol direto num template literal, o que também lançaria TypeError ao rodar. Por isso a linha do symbol usa String(idInterno).
02Os limites do number no boletim
// Notas escolares: o que o tipo number guarda (e não guarda) com exatidão.
const nota: number = 7.9;
const bonusParticipacao: number = 0.1;
const bonusTarefa: number = 0.2;
// Decimais são guardados em binário, e alguns não têm representação exata.
const bonus: number = bonusParticipacao + bonusTarefa;
console.log(`Bônus somado: ${bonus}`);
console.log(`bonus === 0.3? ${bonus === 0.3}`);
// Compare decimais por uma tolerância, não com ===.
console.log(`Quase igual a 0.3? ${Math.abs(bonus - 0.3) < Number.EPSILON}`);
// Arredonde só na hora de exibir.
const final: number = nota + bonus;
console.log(`Nota final: ${final} -> ${final.toFixed(1)}`);
console.log(`É inteira? ${Number.isInteger(final)}`);
// Dividir por zero não lança erro: o resultado é Infinity.
const alunosPresentes: number = 0;
const mediaDaTurma: number = 42 / alunosPresentes;
console.log(`42 / 0 = ${mediaDaTurma}, finito? ${Number.isFinite(mediaDaTurma)}`);
// Acima deste valor, number já não distingue inteiros vizinhos.
const limite: number = Number.MAX_SAFE_INTEGER;
console.log(`Maior inteiro seguro: ${limite}`);
console.log(`limite + 1 === limite + 2? ${limite + 1 === limite + 2}`);
console.log(`limite + 1 é seguro? ${Number.isSafeInteger(limite + 1)}`);
0.1 + 0.2 não dá exatamente 0.3, porque esses decimais não têm representação exata em binário. A comparação segura usa uma tolerância (Number.EPSILON é a menor diferença representável perto de 1), e o arredondamento com toFixed(1) fica para a exibição. Dividir por zero não interrompe o programa: o resultado é Infinity, e Number.isFinite detecta isso.
As últimas linhas mostram o teto dos inteiros exatos. Depois de Number.MAX_SAFE_INTEGER, somar 1 ou 2 dá o mesmo resultado, e Number.isSafeInteger avisa que o valor já não é confiável; é aí que entra o bigint.
03Literais, template literals e symbol no acervo
// Acervo da biblioteca: tipos literais, template literals e symbol.
// let pode mudar, então o tipo é o primitivo inteiro: string.
let situacao = 'disponível'; // tipo string
situacao = 'emprestado';
// const nunca muda, então o tipo é o próprio valor: o literal 14.
const PRAZO_DIAS = 14; // tipo 14
const SETOR = 'Romance'; // tipo 'Romance'
// Anotação com literal: só aceita exatamente estes valores.
let estante: 'A' | 'B' = 'A';
estante = 'B';
// estante = 'C'; // erro: '"C"' não é atribuível a '"A" | "B"'
// Template literal: crases, ${} para expressões e várias linhas.
const titulo: string = 'Memórias Póstumas de Brás Cubas';
const ficha: string = `Livro: ${titulo}
Setor: ${SETOR} | Estante: ${estante}
Situação: ${situacao} | Prazo: ${PRAZO_DIAS} dias (com renovação: ${PRAZO_DIAS * 2})`;
console.log(ficha);
// Dois symbols com a mesma descrição continuam diferentes.
const exemplar1: symbol = Symbol('Brás Cubas');
const exemplar2: symbol = Symbol('Brás Cubas');
console.log(`Mesmo exemplar? ${exemplar1 === exemplar2}`);
console.log(`Descrição: ${exemplar1.description}`);
// Já duas strings iguais são o mesmo valor.
console.log(`Mesmo título? ${titulo === 'Memórias Póstumas de Brás Cubas'}`);
Passe o mouse sobre as variáveis num editor com TypeScript: situacao é string, porque let pode mudar, mas PRAZO_DIAS é o tipo literal 14 e SETOR é 'Romance', porque uma const nunca muda. A anotação 'A' | 'B' restringe estante a esses dois valores, e o tsc recusa 'C'.
O template literal entre crases monta a ficha em três linhas, com expressões como ${PRAZO_DIAS * 2} dentro do texto. No fim, dois symbol com a mesma descrição são diferentes, enquanto duas strings com o mesmo conteúdo são iguais: é isso que torna o symbol útil como identificador único.
Agora é com você
Tente resolver no rascunho antes de abrir a resposta. Os códigos não são executados nesta página.
01Grãos de arroz no tabuleiroFácil
Na lenda do tabuleiro de xadrez, a primeira casa recebe 1 grão de arroz e cada casa seguinte recebe o dobro da anterior. Mostre os grãos das casas 1, 10, 32 e 64, o total das 64 casas e o que acontece com a casa 64 se ela for calculada com number.
- Criar
function graos(casa: number): bigintque devolva2nelevado acasa - 1. - Guardar os valores em constantes anotadas como
biginte calcular o total como2n ** 64n - 1n. - Imprimir a casa 64 calculada com
number(2 ** 63) e otypeofdo total, exatamente como na saída esperada.
function graos(casa: number): bigint {
// a casa n tem 2 elevado a (n - 1) grãos
return 0n;
}
// mostre as casas 1, 10, 32 e 64 e o total do tabuleiro
Casa 1: 1 grão(s)
Casa 10: 512 grão(s)
Casa 32: 2147483648 grão(s)
Casa 64: 9223372036854775808 grão(s)
Total no tabuleiro: 18446744073709551615
Casa 64 com number: 9223372036854776000
Tipo do total: bigint
Ver dica
O operador ** não mistura bigint com number; a função BigInt() converte um número para bigint.
Ver resposta
// Grãos de arroz no tabuleiro: cada casa tem o dobro da anterior.
// A casa n tem 2^(n-1) grãos; BigInt() converte o número da casa.
function graos(casa: number): bigint {
return 2n ** BigInt(casa - 1);
}
const casa1: bigint = graos(1);
const casa10: bigint = graos(10);
const casa32: bigint = graos(32);
const casa64: bigint = graos(64);
console.log(`Casa 1: ${casa1} grão(s)`);
console.log(`Casa 10: ${casa10} grão(s)`);
console.log(`Casa 32: ${casa32} grão(s)`);
console.log(`Casa 64: ${casa64} grão(s)`);
// A soma das 64 casas é 2^64 - 1.
const total: bigint = 2n ** 64n - 1n;
console.log(`Total no tabuleiro: ${total}`);
// O mesmo cálculo com number perde os últimos dígitos.
const casa64Number: number = 2 ** 63;
console.log(`Casa 64 com number: ${casa64Number}`);
console.log(`Tipo do total: ${typeof total}`);
Passo a passo
graosrecebe o número da casa comonumbere devolvebigint. A anotação de retorno deixa claro que o resultado pode ser enorme.BigInt(casa - 1)converte o expoente parabigint, porque o operador**não aceita misturarbigintcomnumber.2né um literalbigint: onno fim muda o tipo do valor, não só a aparência.- As quatro constantes recebem a anotação
: biginte são impressas com template literals, que mostram todos os dígitos. - O total das 64 casas é
2n ** 64n - 1n; todos os operandos têmn, então a conta inteira acontece embigint. 2 ** 63comnumberimprime9223372036854776000: o valor passou do maior inteiro seguro e os últimos dígitos foram arredondados.typeof totalconfirma em tempo de execução que o valor é umbigint.
Por que funciona
O number guarda inteiros exatos só até Number.MAX_SAFE_INTEGER (cerca de 9 quatrilhões). A casa 64 tem mais de 9 quintilhões de grãos, mil vezes esse limite. O bigint não tem teto fixo: ele cresce conforme o valor, então cada dígito do resultado é exato. Como o TypeScript trata bigint e number como tipos diferentes, o tsc obriga a converter de um para o outro de forma explícita.
Erros comuns
- Escrever
2n ** (casa - 1): otscacusa TS2365 ("Operator '**' cannot be applied to types 'bigint' and 'number'") e o Node lançaTypeError: Cannot mix BigInt and other types. Converta comBigInt(...). - Calcular o total com
2 ** 64 - 1: não há erro, mas o resultado é18446744073709552000, com os últimos dígitos errados. Use2n ** 64n - 1n. - Anotar o retorno como
): numbere devolver2n ** ...: erro TS2322, porquebigintnão é atribuível anumber.
Outra forma de resolver
Em vez da potência, dá para dobrar um acumulador let grao: bigint = 1n; com grao *= 2n; casa a casa. Essa versão é preferível quando você precisa passar por todas as casas (por exemplo, para imprimir o tabuleiro inteiro); a potência é mais direta quando só algumas casas interessam.
Saída esperada
Casa 1: 1 grão(s)
Casa 10: 512 grão(s)
Casa 32: 2147483648 grão(s)
Casa 64: 9223372036854775808 grão(s)
Total no tabuleiro: 18446744073709551615
Casa 64 com number: 9223372036854776000
Tipo do total: bigint
02Carrinho da papelaria em centavosMédio
Um carrinho da papelaria tem caneta (R$ 1,10), lápis (R$ 2,20), borracha (R$ 0,60) e marca-texto (R$ 1,15). Mostre o erro da soma direta em reais e calcule o total exato convertendo cada preço para centavos inteiros.
- Criar
function paraCentavos(preco: number): numberque useMath.round(preco * 100). - Imprimir a soma direta dos preços e o resultado de
1.15 * 100sem arredondar. - Acumular os centavos em
let totalCentavos: number = 0com+=e conferir comNumber.isInteger. - Exibir o total em reais com
(totalCentavos / 100).toFixed(2).
function paraCentavos(preco: number): number {
// converta reais em centavos inteiros
return 0;
}
const caneta: number = 1.1;
const lapis: number = 2.2;
const borracha: number = 0.6;
const marcaTexto: number = 1.15;
// compare a soma direta com a soma em centavos
Soma direta: 5.050000000000001
1.15 * 100 = 114.99999999999999
Arredondado: 115
Total em centavos: 505
É inteiro? true
Total: R$ 5.05
Ver dica
Multiplicar por 100 não basta: o resultado pode sair um pouco abaixo do inteiro, e é preciso arredondar para o mais próximo.
Ver resposta
// Carrinho da papelaria: somar em centavos evita erros de arredondamento.
// Converte reais em centavos inteiros; Math.round corrige o resto binário.
function paraCentavos(preco: number): number {
return Math.round(preco * 100);
}
const caneta: number = 1.1;
const lapis: number = 2.2;
const borracha: number = 0.6;
const marcaTexto: number = 1.15;
// Soma direta em reais: os pequenos erros aparecem.
const somaDireta: number = caneta + lapis + borracha + marcaTexto;
console.log(`Soma direta: ${somaDireta}`);
// Multiplicar por 100 também não é exato.
console.log(`1.15 * 100 = ${marcaTexto * 100}`);
console.log(`Arredondado: ${paraCentavos(marcaTexto)}`);
// Soma em centavos: só inteiros, então o resultado é exato.
let totalCentavos: number = 0;
totalCentavos += paraCentavos(caneta);
totalCentavos += paraCentavos(lapis);
totalCentavos += paraCentavos(borracha);
totalCentavos += paraCentavos(marcaTexto);
console.log(`Total em centavos: ${totalCentavos}`);
console.log(`É inteiro? ${Number.isInteger(totalCentavos)}`);
console.log(`Total: R$ ${(totalCentavos / 100).toFixed(2)}`);
Passo a passo
paraCentavosmultiplica o preço por 100 e usaMath.roundpara chegar ao inteiro mais próximo, apagando o resto que a representação binária deixa.- Os quatro preços são
numbercom anotação explícita, como viriam de uma tabela da loja. - A soma direta em reais imprime
5.050000000000001: cada decimal carrega um erro minúsculo, e a soma acumula esses erros. - A linha
1.15 * 100mostra que até a multiplicação sai114.99999999999999, por isso o arredondamento é obrigatório. totalCentavosélete começa em0; cada+=soma um inteiro, e somar inteiros pequenos comnumberé sempre exato.Number.isIntegerconfirma que o total continua inteiro.- Só na exibição o total volta para reais com
/ 100etoFixed(2), que devolve o texto com duas casas.
Por que funciona
O tipo number segue o padrão IEEE 754 de ponto flutuante: valores como 1.1 e 0.6 não têm representação exata em binário, assim como 1/3 não tem em decimal. Inteiros até Number.MAX_SAFE_INTEGER, por outro lado, são exatos. Trabalhar em centavos troca contas com decimais por contas com inteiros, e o arredondamento acontece uma única vez, na entrada de cada preço.
Erros comuns
- Converter com
Math.floor(preco * 100): para1.15o resultado é114, e o total fica um centavo menor. UseMath.round. - Guardar o texto formatado no acumulador, como
totalCentavos = somaDireta.toFixed(2);: erro TS2322, porquetoFixeddevolvestringe a variável énumber. - Conferir o total com
somaDireta === 5.05: dáfalse. Compare inteiros em centavos ou use uma tolerância comNumber.EPSILON.
Outra forma de resolver
Os preços podem ser guardados já em centavos desde o início (const caneta: number = 110;), o que dispensa paraCentavos. É a escolha preferível em sistemas reais de loja, porque o valor nunca passa por um decimal; a conversão só é necessária quando os preços chegam em reais de outra fonte.
Saída esperada
Soma direta: 5.050000000000001
1.15 * 100 = 114.99999999999999
Arredondado: 115
Total em centavos: 505
É inteiro? true
Total: R$ 5.05
03Etiquetas do acervo com tipos literaisDesafio
A biblioteca identifica cada livro por um código no formato setor-número, como ROM-12, e os setores são só ROM, POE e HIS. Crie tipos que garantam esse formato, gere os códigos de três livros e imprima as etiquetas, mostrando também que o código é uma string comum ao rodar.
- Declarar
type Setor = 'ROM' | 'POE' | 'HIS'e o tipo template literaltype Codigo =${Setor}-${number}``. - Criar
function gerarCodigo(setor: Setor, numero: number): Codigoque monte o código com um template literal. - Montar cada etiqueta com um template literal de duas linhas; livros raros são "só consulta", os demais mostram o limite
const LIMITE = 3. - Imprimir o
typeofde um código e se ele começa com o setor, exatamente como na saída esperada.
type Setor = 'ROM' | 'POE' | 'HIS';
// declare o tipo Codigo no formato setor-número
const LIMITE = 3;
// crie gerarCodigo e etiqueta e imprima as três etiquetas:
// ROM 12 Senhora (não raro), POE 7 Lira dos Vinte Anos (raro),
// HIS 203 Casa-Grande & Senzala (não raro)
[ROM-12] Senhora
raro: false | empréstimo: até 3 livros
[POE-7] Lira dos Vinte Anos
raro: true | empréstimo: só consulta
[HIS-203] Casa-Grande & Senzala
raro: false | empréstimo: até 3 livros
typeof ROM-12: string
Começa com o setor? true
Ver dica
Um texto montado com + vira string genérica; anotar o retorno da função e devolver um template literal direto permite ao tsc conferir o formato.
Ver resposta
// Etiquetas do acervo: tipos literais e tipos template literal.
// Só três setores existem: um tipo união de literais string.
type Setor = 'ROM' | 'POE' | 'HIS';
// Todo código é um setor, um hífen e um número, como 'ROM-12'.
type Codigo = `${Setor}-${number}`;
// const com literal: o tipo de LIMITE é exatamente 3.
const LIMITE = 3;
function gerarCodigo(setor: Setor, numero: number): Codigo {
return `${setor}-${numero}`;
}
// Template literal de várias linhas monta a etiqueta.
function etiqueta(codigo: Codigo, titulo: string, raro: boolean): string {
return `[${codigo}] ${titulo}
raro: ${raro} | empréstimo: ${raro ? 'só consulta' : `até ${LIMITE} livros`}`;
}
const romance: Codigo = gerarCodigo('ROM', 12);
const poesia: Codigo = gerarCodigo('POE', 7);
const historia: Codigo = gerarCodigo('HIS', 203);
console.log(etiqueta(romance, 'Senhora', false));
console.log(etiqueta(poesia, 'Lira dos Vinte Anos', true));
console.log(etiqueta(historia, 'Casa-Grande & Senzala', false));
// Os tipos somem ao rodar: o código é uma string comum.
console.log(`typeof ${romance}: ${typeof romance}`);
console.log(`Começa com o setor? ${romance.startsWith('ROM')}`);
// const invalido: Codigo = 'FIC-1'; // erro: 'FIC' não é um Setor
// gerarCodigo('ROM', '12'); // erro: string não é atribuível a number
Passo a passo
Setoré uma união de tipos literais: uma variável desse tipo só aceita'ROM','POE'ou'HIS', e não qualquerstring.Codigoé um tipo template literal: ele descreve o formato do texto, umSetor, um hífen e umnumber. Otscconfere esse formato em cada atribuição.const LIMITE = 3fica com o tipo literal3, porque umaconstnunca muda; comlet, o tipo serianumber.gerarCodigodevolve um template literal. Como o retorno está anotado comoCodigo, otscmonta o tipo da expressão a partir das partes (Setorenumber) e aceita o valor.etiquetausa um template literal de duas linhas e um ternário dentro de${}para escolher o texto do empréstimo, com outro template literal aninhado para mostrar oLIMITE.- As três constantes recebem a anotação
: Codigoe são usadas para imprimir as etiquetas. typeof romanceimprimestring: tipos literais e template literal existem só para otsce são apagados ao rodar.startsWithfunciona normalmente, porque em tempo de execução o código é uma string comum.
Por que funciona
Tipos literais são subtipos dos primitivos: 'ROM' é uma string, e `ROM-${number}` também, só que mais restrita. Isso deixa o compilador recusar códigos fora do padrão, como 'FIC-1' ou 'rom-12', antes do programa rodar, sem custo nenhum em tempo de execução. A função gerarCodigo concentra a montagem do formato num só lugar, e quem a chama não consegue passar um setor inexistente nem um número em forma de texto.
Erros comuns
- Montar o código com
return setor + '-' + numero;: o resultado éstringe otscacusa TS2322, poisstringnão é atribuível aCodigo. Use um template literal noreturn. - Guardar o texto antes numa variável sem anotação, como
const texto =${setor}-${numero}; return texto;: o tipo detextoé alargado parastringe o mesmo TS2322 aparece. Devolva a expressão direto ou anoteconst texto: Codigo. - Escrever
const romance: Codigo = 'rom-12';: erro TS2322, porque os literais diferenciam maiúsculas de minúsculas e'rom'não é umSetor. - Chamar
gerarCodigo('ROM', '12'): erro TS2345, já que o segundo parâmetro énumber.
Outra forma de resolver
Com as const é possível declarar os setores numa lista e derivar o tipo dela: const SETORES = ['ROM', 'POE', 'HIS'] as const; e type Setor = (typeof SETORES)[number];. Isso é preferível quando o programa também precisa percorrer os setores em tempo de execução, porque a lista e o tipo ficam sempre sincronizados.
Saída esperada
[ROM-12] Senhora
raro: false | empréstimo: até 3 livros
[POE-7] Lira dos Vinte Anos
raro: true | empréstimo: só consulta
[HIS-203] Casa-Grande & Senzala
raro: false | empréstimo: até 3 livros
typeof ROM-12: string
Começa com o setor? true