Tipagem estrutural vs nominal: como TypeScript e Go diferem no sistema de tipos

1. Fundamentos dos sistemas de tipos: estrutural vs nominal

A tipagem estrutural determina a compatibilidade entre tipos com base na sua forma — ou seja, na estrutura dos membros que possuem. Se dois tipos têm os mesmos campos e métodos, eles são considerados compatíveis, independentemente de seus nomes ou declarações explícitas.

A tipagem nominal, por outro lado, exige que a compatibilidade seja declarada explicitamente. Dois tipos são compatíveis apenas se um herda do outro ou se ambos implementam a mesma interface de forma declarada.

// Pseudocódigo: Tipagem estrutural
type Pessoa = { nome: string, idade: number }
type Animal = { nome: string, idade: number }

// São compatíveis porque têm a mesma estrutura
função cumprimentar(entidade: Pessoa) { ... }
cumprimentar({ nome: "Rex", idade: 3 } as Animal) // OK

// Pseudocódigo: Tipagem nominal
type Pessoa = { nome: string, idade: number }
type Animal = { nome: string, idade: number }

// NÃO são compatíveis, pois são tipos diferentes
função cumprimentar(entidade: Pessoa) { ... }
cumprimentar({ nome: "Rex", idade: 3 } as Animal) // ERRO

2. Tipagem estrutural em TypeScript: flexibilidade e duck typing

TypeScript adota a tipagem estrutural como seu mecanismo central. A verificação de tipos baseia-se exclusivamente na presença dos membros necessários.

interface Usuario {
  nome: string;
  email: string;
}

interface Funcionario {
  nome: string;
  email: string;
  cargo: string;
}

function enviarEmail(usuario: Usuario): void {
  console.log(`Email enviado para ${usuario.email}`);
}

const funcionario: Funcionario = {
  nome: "João",
  email: "joao@empresa.com",
  cargo: "Desenvolvedor"
};

// TypeScript aceita porque Funcionario tem todos os membros de Usuario
enviarEmail(funcionario); // OK

Isso reduz drasticamente o boilerplate. Não é necessário declarar herança ou implementação explícita. No entanto, há riscos: compatibilidade acidental pode ocorrer quando dois tipos não relacionados compartilham acidentalmente a mesma estrutura.

interface Coordenada {
  x: number;
  y: number;
}

interface Temperatura {
  x: number; // representa valor
  y: number; // representa unidade
}

function calcularDistancia(ponto: Coordenada): number {
  return Math.sqrt(ponto.x ** 2 + ponto.y ** 2);
}

const temp: Temperatura = { x: 25, y: 0 };
calcularDistancia(temp); // Compila, mas é semânticamente errado!

3. Tipagem nominal em Go: segurança e explicitude

Go adota um sistema nominal, mas com uma particularidade: a implementação de interfaces é implícita. Um tipo implementa uma interface automaticamente se possuir todos os métodos exigidos, mas a verificação de compatibilidade entre tipos concretos é nominal.

type Usuario struct {
    Nome  string
    Email string
}

type Funcionario struct {
    Nome  string
    Email string
    Cargo string
}

func enviarEmail(u Usuario) {
    fmt.Printf("Email enviado para %s\n", u.Email)
}

func main() {
    f := Funcionario{
        Nome:  "João",
        Email: "joao@empresa.com",
        Cargo: "Desenvolvedor",
    }

    // ERRO: cannot use f (variable of type Funcionario) as type Usuario
    enviarEmail(f)
}

Para resolver, é necessário usar uma interface:

type EmailSender interface {
    Enviar()
}

type Usuario struct {
    Nome  string
    Email string
}

func (u Usuario) Enviar() {
    fmt.Printf("Email enviado para %s\n", u.Email)
}

type Funcionario struct {
    Nome  string
    Email string
    Cargo string
}

func (f Funcionario) Enviar() {
    fmt.Printf("Email enviado para %s\n", f.Email)
}

func processarRemetente(s EmailSender) {
    s.Enviar()
}

func main() {
    u := Usuario{Nome: "Maria", Email: "maria@email.com"}
    f := Funcionario{Nome: "João", Email: "joao@empresa.com", Cargo: "Dev"}

    processarRemetente(u) // OK
    processarRemetente(f) // OK
}

4. Comparação prática: quando cada abordagem brilha

TypeScript é ideal para cenários com APIs dinâmicas, bibliotecas de terceiros e evolução rápida de tipos. A flexibilidade estrutural permite integração fácil com JavaScript e JSON.

Go brilha em sistemas críticos, manutenção de longo prazo e ambientes onde segurança de tipos estrita é essencial. A tipagem nominal evita erros sutis de compatibilidade acidental.

// TypeScript: Mesmo problema resolvido com tipagem estrutural
interface DadosAPI {
    id: number;
    nome: string;
    valor: number;
}

function processarDados(dados: DadosAPI) {
    // Processa dados da API
}

// Go: Mesmo problema resolvido com tipagem nominal
type DadosAPI struct {
    ID    int
    Nome  string
    Valor float64
}

type Processador interface {
    Processar(DadosAPI) error
}

5. Tratamento de tipos primitivos e literais

TypeScript permite tipos literais e uniões de tipos, estendendo a tipagem estrutural para valores específicos:

type Status = "ativo" | "inativo" | "pendente";
type ID = string | number;

function atualizarStatus(id: ID, status: Status): void {
    console.log(`Status ${status} para ID ${id}`);
}

Go trata tipos nomeados a partir de primitivos como tipos distintos:

type Celsius float64
type Fahrenheit float64

func converter(c Celsius) Fahrenheit {
    // ERRO: cannot use c * 9/5 + 32 (type float64) as type Fahrenheit
    // return Fahrenheit(c * 9/5 + 32)
    return Fahrenheit(float64(c) * 9/5 + 32)
}

Isso oferece segurança adicional em operações matemáticas, evitando misturar unidades.

6. Interfaces e contratos: polimorfismo nas duas abordagens

Em TypeScript, interfaces funcionam como contratos estruturais. Qualquer objeto com a forma correta satisfaz a interface:

interface Saudacao {
    saudar(): string;
}

const pessoa = { saudar: () => "Olá!" };
const animal = { saudar: () => "Au au!" };

function cumprimentar(s: Saudacao): void {
    console.log(s.saudar());
}

cumprimentar(pessoa); // OK
cumprimentar(animal); // OK

Em Go, interfaces são contratos nominais implícitos. Apenas tipos que implementam explicitamente os métodos satisfazem a interface:

type Saudacao interface {
    Saudar() string
}

type Pessoa struct{}
func (p Pessoa) Saudar() string { return "Olá!" }

type Animal struct{}
func (a Animal) Saudar() string { return "Au au!" }

func cumprimentar(s Saudacao) {
    fmt.Println(s.Saudar())
}

7. Vantagens e desvantagens no ecossistema real

TypeScript oferece flexibilidade para integração com JavaScript, mas é propenso a erros silenciosos quando tipos mudam ou quando há compatibilidade acidental. É ideal para projetos web e aplicações que evoluem rapidamente.

Go oferece clareza e previsibilidade, mas é mais verboso em cenários de composição. É ideal para sistemas críticos, microserviços e infraestrutura.

Para projetos web com JavaScript existente, TypeScript é a escolha natural. Para sistemas de backend que exigem segurança e desempenho, Go é superior.

8. Boas práticas e armadilhas comuns

Em TypeScript, evite dependência excessiva em tipos estruturais para interfaces críticas. Use marcas nominais quando necessário:

// Marca nominal para evitar compatibilidade acidental
type ID = string & { readonly __brand: unique symbol };

Em Go, use interfaces pequenas e design explícito para evitar acoplamento desnecessário:

type Leitor interface {
    Ler() ([]byte, error)
}

type Escritor interface {
    Escrever([]byte) (int, error)
}

Para migrar entre paradigmas, entenda que TypeScript prioriza flexibilidade enquanto Go prioriza segurança. Trabalhar com ambas as linguagens exige adaptar o pensamento sobre tipos.


Referências