undefined x null: quando usar e a melhor estratégia para representar a ausência de valores no Typescript

undefined e null parecem representar a mesma coisa, mas a forma como você utiliza cada um pode impactar diretamente a legibilidade do código, a tipagem no TypeScript e a complexidade do seu projeto. Neste artigo, vamos entender as diferenças entre eles, quando cada um faz sentido e qual estratégia adotamos para representar a ausência de valores de forma consistente.
Typescript
Compartilhar:
undefined x null: quando usar e a melhor estratégia para representar a ausência de valores no Typescript

Uma das primeiras dúvidas de quem começa a trabalhar com JavaScript e TypeScript é:

Qual a diferença entre undefined e null?

Embora ambos representem a ausência de um valor, eles possuem origens diferentes e comportamentos distintos dentro da linguagem.

Na prática, essa é uma decisão que parece pequena, mas que impacta diretamente a legibilidade do código, a modelagem das tipagens, a quantidade de verificações condicionais e até mesmo a forma como uma equipe desenvolve novas funcionalidades.

É comum encontrar projetos onde undefined e null são utilizados de maneira indiscriminada. Em alguns arquivos uma propriedade opcional retorna undefined; em outros, a mesma informação é representada por null. À medida que a aplicação cresce, essa inconsistência começa a gerar código defensivo, tipagens mais complexas e dúvidas recorrentes durante o desenvolvimento.

Mas afinal:

  • Existe uma escolha melhor?

  • Quando realmente faz sentido utilizar null?

  • O próprio JavaScript incentiva o uso de um deles?

  • Como essa decisão impacta projetos escritos em TypeScript?

Neste artigo vamos responder essas perguntas e entender qual estratégia utilizamos para representar ausência de valores em nossos projetos.


O JavaScript possui duas formas de representar ausência de valor

Quando trabalhamos com JavaScript, existem dois valores que normalmente são utilizados para indicar que uma informação não existe: undefinedenull.

À primeira vista eles parecem equivalentes.

const user = {
  avatar: undefined,
}

const author = {
  avatar: null,
}

Nos dois casos, o avatar não existe.

Mas será que realmente precisamos de duas formas diferentes para representar exatamente a mesma ideia?

Antes de responder essa pergunta, vale entender como cada um surgiu.


Como undefined e null surgiram?

O undefined faz parte do funcionamento natural da linguagem.

Sempre que algum valor não existir, o próprio JavaScript retorna undefined.

Variáveis não inicializadas:

let name

console.log(name)
// undefined

Propriedades inexistentes:

const user = {}

console.log(user.avatar)
// undefined

Parâmetros opcionais:

function createPost(title?: string) {
  console.log(title)
}

createPost()
// undefined

Retornos sem valor:

function save() {}

console.log(save())
// undefined

Perceba que não fizemos absolutamente nada para obter esse comportamento.

O próprio JavaScript já utiliza undefined para representar ausência de informação.


O null funciona de maneira diferente.

Ele nunca aparece automaticamente.

Sempre existe alguém atribuindo esse valor.

const user = {
  avatar: null,
}

Essa diferença parece pequena, mas muda completamente a forma como modelamos nossas aplicações.


O verdadeiro problema não é o null

Até aqui, nenhum dos dois parece melhor.

O problema começa quando os dois passam a representar exatamente o mesmo estado dentro do projeto.

Imagine a seguinte interface.

interface User {
  avatar?: string | null
}

Agora essa propriedade pode assumir três estados diferentes.

avatar = "avatar.png"

avatar = undefined

avatar = null

Sempre que outro desenvolvedor consumir essa propriedade, ele precisará responder uma pergunta:

O que significa undefined? E o que significa null?

Se ambos representam apenas "não existe valor", por que precisamos dos dois?


A complexidade cresce rapidamente

Adicionar null em uma propriedade parece uma decisão inofensiva.

Mas ela se espalha por toda a aplicação.

Agora precisamos refletir esse comportamento:

  • nos DTOs;

  • nas entidades;

  • nos componentes Vue;

  • nas Stores;

  • nos composables;

  • nos schemas do Zod;

  • nos testes;

  • nas validações.

Uma simples propriedade opcional passa a exigir verificações adicionais.

if (
  user.avatar !== null &&
  user.avatar !== undefined
) {
  // ...
}

Ou então:

const avatar =
  user.avatar === null
    ? "/default.png"
    : user.avatar

Esse padrão acaba sendo repetido centenas de vezes ao longo do projeto.


O TypeScript já possui uma forma elegante de resolver isso

O próprio TypeScript oferece uma maneira muito simples de representar um valor opcional.

interface Post {
  subtitle?: string
}

O operador ? já comunica exatamente o que precisamos:

Esta propriedade pode não existir.

Agora compare com:

interface Post {
  subtitle?: string | null
}

Além de aumentar a tipagem, adicionamos mais um estado que deverá ser tratado por todos os consumidores.

Essa pequena decisão aumenta a carga cognitiva de quem lê o código.


Recursos modernos ficam mais previsíveis

Recursos como Optional Chaining e Nullish Coalescing tornam o código extremamente elegante.

const city = user.address?.city
const avatar = user.avatar ?? "/default.png"

Quanto menor a quantidade de estados possíveis para uma propriedade, mais simples e previsível se torna sua utilização.


O efeito nos Schemas

Essa diferença também aparece quando utilizamos bibliotecas como Zod.

Sem null:

const PostSchema = z.object({
  title: z.string(),
  subtitle: z.string().optional(),
})

Com null:

const PostSchema = z.object({
  title: z.string(),
  subtitle: z.string().optional().nullable(),
})

A leitura fica mais complexa e qualquer transformação passa a considerar dois estados para representar exatamente a mesma ausência de informação.


Então devemos abolir o null?

Não.

Existem situações em que você simplesmente não controla o contrato.

Por exemplo:

  • APIs de terceiros;

  • bancos de dados relacionais;

  • GraphQL;

  • sistemas legados.

Nesses cenários é perfeitamente comum receber null.

O importante é impedir que ele se espalhe pelo restante da aplicação.

Uma abordagem bastante eficiente consiste em normalizar os dados ainda na camada de infraestrutura.

function toPost(data: ApiPost) {
  return {
    ...data,
    subtitle: data.subtitle ?? undefined,
  }
}

A partir desse momento, todo o domínio passa a trabalhar com uma única representação para ausência de valores.


A convenção que eu sigo

Depois de analisar como a linguagem funciona e os impactos dessa decisão, geralmente adoto uma convenção simples nos projetos em que atuo.

Dentro da aplicação utilizo apenas undefined para representar ausência de valores.

Quando consumimos APIs, bancos de dados ou sistemas legados que retornam null, realizamos a conversão na camada de infraestrutura antes que esses dados cheguem ao domínio.

Com isso conseguimos:

  • reduzir código defensivo;

  • simplificar as tipagens;

  • deixar DTOs e entidades mais consistentes;

  • escrever schemas menores;

  • facilitar a manutenção do projeto;

  • reduzir a carga cognitiva da equipe.

Mais importante do que escolher entre undefined ou null é definir uma convenção e aplicá-la de forma consistente.

No nosso caso, escolhemos trabalhar apenas com undefined dentro da aplicação porque acreditamos que isso torna o código mais previsível, mais simples e mais fácil de evoluir.

Gostou do artigo? Compartilhe!

Últimos artigos

Ver todos