Null safety
Escopo: JavaScript. Visão transversal: shared/standards/null-safety.md.
JavaScript não tem um compilador que avise, antes de rodar, quando um valor pode ser nulo. Essa nullability (possibilidade de o valor ser nulo ou indefinido) fica por conta do código: validar nos boundaries (limites, os pontos onde dados externos entram) e confiar no interior a partir daí. Os operadores ?? e ?. deixam essa intenção explícita. Escolher o operador errado, || no lugar de ??, descarta valores válidos como 0 e "", o tipo de bug que costuma aparecer só em produção.
Conceito geral: Null Safety
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| null (nulo) | Valor explícito que indica "ausência intencional"; atribuído pelo programador |
| undefined (indefinido) | Valor padrão de variável não inicializada ou propriedade inexistente; atribuído pela engine |
| nullish (ausente: nulo ou indefinido) | Conjunto que reúne null e undefined; o que ?? e ?. tratam |
| falsy (avalia como falso) | Valores que avaliam como false em booleano: null, undefined, 0, "", false, NaN |
nullish coalescing (coalescência de ausente, ??) | Retorna o lado direito apenas se o esquerdo for null ou undefined |
optional chaining (encadeamento opcional, ?.) | Acessa propriedade ou chama método sem lançar erro se a base for nullish |
| boundary (limite) | Ponto onde dados externos entram (HTTP, DB, fila); local correto para validar nulos |
| non-null assertion (afirmação de não-nulo) | Garantia explícita ao leitor de que o valor não é nulo neste ponto; em JS via comentário ou guard |
Valor padrão sem descartar 0 e ""
|| devolve o lado direito para qualquer valor falsy (que avalia como falso): 0, "" e
false já disparam o valor alternativo. ?? devolve o lado direito só quando o esquerdo é
null ou undefined. Para preencher um valor padrão, ?? é o certo na maioria dos casos.
❌ Ruim: || descarta valores falsy válidos
const timeout = config.timeout || 5000; // 0 → 5000: zero é tempo válido
const retries = input.retries || 3; // 0 → 3: zero retries é intencional
const debug = options.debug || false; // false → false: ok aqui, mas por acidente
✅ Bom: ?? respeita 0, "" e false
const timeout = config.timeout ?? 5000;
const retries = input.retries ?? 3;
const port = process.env.PORT ?? config.port ?? 3000; // encadeamento de fallbacks
Atribuir um padrão só quando o valor é nulo
??= atribui só se o valor atual for null ou undefined. ||= atribui para qualquer falsy.
É a mesma distinção entre ?? e ||, agora na forma de atribuição.
❌ Ruim: ||= sobrescreve zero, que é um valor válido
let count = 0;
count ||= 10; // count vira 10: zero é falsy, então ||= dispara
✅ Bom: ??= respeita zero e false
let count = 0;
count ??= 10; // count permanece 0: zero não é null
const config = {};
config.port ??= 3000;
config.port ??= 8080; // não executa: port já é 3000
Quando ?. ajuda e quando esconde um bug
?. devolve undefined se o receptor for null ou undefined, sem lançar exceção.
O lugar dele é o campo opcional por natureza. Quando o campo deveria sempre existir, a ausência
é um bug, e abafá-la com ?. só adia a descoberta: use uma guard clause que falha na hora.
❌ Ruim: ?. esconde contrato fraco
async function getOrderTotal(orderId) {
const order = await db.orders.findById(orderId);
return order?.items?.reduce((sum, item) => sum + item.price, 0) ?? 0;
// se order não existe, retorna 0 silenciosamente. É isso que queremos?
}
✅ Bom: guard clause quando ausência é erro; ?. quando é esperada
// ausência é erro → guard clause
async function getOrderTotal(orderId) {
const order = await orderRepository.findById(orderId);
if (!order) throw new NotFoundError({ message: `Order ${orderId} not found.` });
const total = order.items.reduce((sum, item) => sum + item.price, 0);
return total;
}
// ausência é esperada → ?. é suficiente
function formatUserCity(user) {
const city = user?.address?.city ?? "N/A";
return city;
}
Coleções nunca são nulas
Funções que retornam listas sempre retornam [], nunca null. No limite com dados externos,
normalize com ?? [].
❌ Ruim: null em lista força defesa no caller
async function findOrdersByUser(userId) {
const orders = await db.orders.findByUser(userId);
return orders.length ? orders : null;
}
✅ Bom: lista vazia como estado neutro
async function findOrdersByUser(userId) {
const orders = await orderRepository.findByUser(userId);
return orders; // ORM já retorna []: nunca null
}
// limite com API externa: normaliza na entrada
async function fetchUserOrders(userId) {
const response = await externalApi.get(`/users/${userId}/orders`);
const orders = response.orders ?? []; // normaliza aqui, não no caller
return orders;
}
flatMap: filtrar e transformar em uma passagem
flatMap que devolve [] nos casos inválidos remove os nulos durante a própria transformação.
Lê melhor que .filter().map() e percorre o array uma única vez em vez de duas.
❌ Ruim: filter + map percorre o array duas vezes
const rawItems = ["1", null, "3", undefined, "5"];
const parsed = rawItems
.filter((item) => item != null)
.map((item) => parseInt(item, 10));
✅ Bom: flatMap filtra e transforma em uma passagem
const rawItems = ["1", null, "3", undefined, "5"];
const parsed = rawItems.flatMap((item) => {
if (item == null) return [];
return [parseInt(item, 10)];
});
// [1, 3, 5]
Object.hasOwn para checar propriedade com segurança
Object.hasOwn(obj, key) verifica se a propriedade existe no próprio objeto, sem cair na
prototype pollution (propriedades injetadas no protótipo por um atacante). Substitui o
padrão antigo obj.hasOwnProperty(key), que pode ser sobrescrito.
❌ Ruim: hasOwnProperty vulnerável a prototype pollution
const config = { timeout: 0 };
config.hasOwnProperty("timeout"); // funciona, mas pode ser sobrescrito via prototype
✅ Bom: Object.hasOwn seguro e direto
const config = { timeout: 0, debug: false };
Object.hasOwn(config, "timeout"); // true: existe, mesmo sendo 0
Object.hasOwn(config, "retries"); // false: não existe
function mergeConfig(defaults, overrides) {
const result = { ...defaults };
for (const key of Object.keys(overrides)) {
if (Object.hasOwn(defaults, key)) {
result[key] = overrides[key] ?? defaults[key];
}
}
return result;
}
structuredClone para cópia profunda
JSON.parse(JSON.stringify(obj)) descarta campos undefined e não preserva Date, Map nem
Set. structuredClone faz a cópia profunda mantendo null e os tipos nativos como estão.
❌ Ruim: JSON round-trip perde undefined, Date e Map
const order = {
notes: null,
tags: undefined,
createdAt: new Date(),
meta: new Map([["source", "web"]]),
};
const clone = JSON.parse(JSON.stringify(order));
// notes: null ✓
// tags ausente: undefined some
// createdAt "2026-...": virou string
// meta {}: Map virou objeto vazio
✅ Bom: structuredClone preserva todos os tipos
const order = {
notes: null,
tags: undefined,
createdAt: new Date(),
meta: new Map([["source", "web"]]),
};
const clone = structuredClone(order);
// notes: null ✓
// tags: undefined ✓
// createdAt: Date ✓
// meta: Map ✓
clone.meta.set("cloned", true); // não afeta o original
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.