Soundness vs completeness no sistema de tipos

1. Introdução aos Conceitos Fundamentais

Em teoria de sistemas de tipos, dois conceitos são fundamentais para avaliar a qualidade de um type checker: soundness (solidez) e completeness (completude). Compreender essa dicotomia é essencial para qualquer desenvolvedor TypeScript que deseje usar a linguagem de forma consciente e produtiva.

Soundness significa que todo programa que passa na verificação de tipos está livre de erros de tipo em tempo de execução. Em outras palavras, se o compilador diz "ok", você pode confiar que não haverá surpresas como undefined is not a function ou Cannot read property 'x' of null.

Completeness significa que todo programa que não possui erros de tipo em tempo de execução é aceito pelo verificador. Um sistema completo nunca rejeita código válido — ele é permissivo o suficiente para aceitar todo programa correto.

O problema é que sistemas de tipos reais precisam fazer um trade-off entre esses dois objetivos. Um sistema perfeitamente sound e completo é impossível (o Teorema da Incompletude de Gödel impõe limites), e mesmo aproximações perfeitas são extremamente complexas. Linguagens precisam escolher: priorizar a segurança (soundness) ou a flexibilidade (completeness).

2. TypeScript: Um Sistema de Tipos Deliberadamente Não-Sound

O TypeScript adota uma posição clara: prioriza a produtividade e a adoção gradual sobre a pureza teórica. Anders Hejlsberg, criador do TypeScript, já afirmou que o objetivo não é ser um sistema de tipos perfeitamente sound, mas sim um que seja útil para desenvolvedores JavaScript.

Essa filosofia se manifesta em várias features que comprometem a soundness:

// Exemplo 1: 'any' — a válvula de escape total
let valor: any = 42;
valor.toUpperCase(); // Sem erro de tipo, mas explode em runtime

// Exemplo 2: Type assertions (as)
const elemento = document.getElementById("meu-botao") as HTMLButtonElement;
// TypeScript assume que você sabe o que está fazendo

// Exemplo 3: Indexação em arrays
const numeros: number[] = [1, 2, 3];
const quarto: number = numeros[3]; // Sem erro, mas retorna undefined

O strictNullChecks é um exemplo de melhoria incremental de soundness. Quando ativado, o TypeScript passa a considerar null e undefined como tipos distintos, forçando verificações:

// Sem strictNullChecks
function comprimento(s: string) {
    return s.length; // Aceito mesmo se s for null
}

// Com strictNullChecks
function comprimentoSeguro(s: string | null) {
    if (s === null) return 0;
    return s.length; // TypeScript sabe que s não é null aqui
}

3. Casos Concretos de Falta de Soundness em TypeScript

Covariance em arrays

Arrays em TypeScript são covariantes, o que significa que Array<string> é considerado subtipo de Array<string | number>. Isso é conveniente, mas permite bugs:

const strings: string[] = ["a", "b", "c"];
const misto: (string | number)[] = strings; // OK para o type checker
misto.push(42); // OK em tempo de compilação
console.log(strings[2].toUpperCase()); // 42.toUpperCase() → runtime error!

Narrowing impreciso

O narrowing (estreitamento de tipos) do TypeScript é inteligente, mas não exaustivo:

function processar(valor: string | number | boolean) {
    if (typeof valor === "string") {
        return valor.toUpperCase();
    }
    if (typeof valor === "number") {
        return valor.toFixed(2);
    }
    // TypeScript assume que o resto é boolean
    // Mas e se valor for um objeto? O narrowing não cobre todos os casos
    return valor ? "true" : "false";
}

Mutações e aliasing

Objetos mutáveis criam armadilhas que o sistema de tipos não consegue prevenir:

interface Usuario {
    nome: string;
    idade: number;
}

function atualizarUsuario(usuario: Usuario) {
    usuario.idade = 50; // Mutação permitida
}

const usuario: Readonly<Usuario> = { nome: "João", idade: 30 };
atualizarUsuario(usuario as Usuario); // Type assertion quebra a proteção

4. Onde TypeScript é Sound (e Onde Não é)

Comparado a linguagens como Haskell, Rust ou Elm, o TypeScript é significativamente menos sound. Essas linguagens impõem regras mais rígidas em troca de garantias mais fortes:

// Em Haskell, isso seria rejeitado em tempo de compilação
let x: number = 10;
x = "texto"; // TypeScript aceita se x for any, Haskell jamais aceitaria

No entanto, é possível criar subconjuntos sound do TypeScript:

// Código sound: sem any, sem type assertions, com strict: true
function somar(a: number, b: number): number {
    return a + b;
}

// Uso de unknown em vez de any
function processarSeguro(valor: unknown): string {
    if (typeof valor === "string") {
        return valor;
    }
    return String(valor);
}

Flags como noUncheckedIndexedAccess aumentam a soundness:

// Com noUncheckedIndexedAccess ativado
const arr: number[] = [1, 2, 3];
const item = arr[5]; // Tipo: number | undefined (antes era number)

5. Completeness: O Lado da Balança

Sistemas sound frequentemente geram falsos positivos — rejeitam código perfeitamente válido. Em Rust, por exemplo, o borrow checker pode rejeitar padrões de código perfeitamente seguros, forçando reestruturações:

// TypeScript aceita este padrão comum
function processarItens(itens: string[] | null) {
    const primeiro = itens?.[0]; // Optional chaining: seguro e aceito
    return primeiro?.toUpperCase();
}

// Em um sistema sound como Rust, padrões similares exigiriam match explícito

TypeScript prioriza a completude: aceitar código válido é mais importante do que rejeitar código inválido. Isso reduz frustração e aumenta produtividade, mas às custas de garantias.

6. Implicações Práticas para o Desenvolvedor TypeScript

Para mitigar a falta de soundness, adote estas estratégias:

// 1. Configure strict: true no tsconfig.json
// 2. Use noImplicitAny (já incluso em strict)
// 3. Prefira unknown a any
function parseJSON(texto: string): unknown {
    return JSON.parse(texto);
}

// 4. Use satisfies em vez de type assertions
type Cores = "red" | "green" | "blue";
const cor = "red" satisfies Cores; // Verifica sem assertion

// 5. Validação em runtime com bibliotecas como zod
import { z } from "zod";

const UsuarioSchema = z.object({
    nome: z.string(),
    idade: z.number().positive()
});

type Usuario = z.infer<typeof UsuarioSchema>;

function salvarUsuario(dados: unknown) {
    const usuario = UsuarioSchema.parse(dados); // Valida em runtime
    // Agora 'usuario' é tipado e seguro
}

7. Comparação com Outras Abordagens

Flow, o type checker do Facebook, adota uma filosofia diferente. Ele é mais sound em áreas específicas (como nullability e variance), mas menos completo em outras. Por exemplo, Flow detecta certos padrões de null que TypeScript não detecta:

// TypeScript aceita, Flow rejeita
function exemplo(x: ?string) {
    return x.length; // Flow exige verificação de null
}

Dart com null safety e C# com dynamic mostram caminhos intermediários: oferecem válvulas de escape (dynamic em C#) enquanto mantêm a maior parte do sistema sound.

8. Conclusão e Perspectivas Futuras

O roadmap do TypeScript mostra melhorias incrementais em soundness. Flags como exactOptionalPropertyTypes e noUncheckedIndexedAccess estão se tornando mais comuns. Propostas futuras incluem pattern matching mais robusto e sealed types.

A escolha pragmática do TypeScript — priorizar completude sobre soundness — é a razão de seu sucesso. Milhões de desenvolvedores JavaScript puderam adotar tipos gradualmente, sem reescrever código existente. Mas cabe a cada desenvolvedor entender os limites do sistema e usar ferramentas complementares quando necessário.

No fim, TypeScript não é um sistema de tipos perfeito — é um sistema de tipos útil. E essa utilidade, combinada com consciência de suas limitações, é o que o torna tão poderoso.

Referências