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
- TypeScript Handbook: Type Compatibility — Documentação oficial explicando como TypeScript verifica compatibilidade estrutural entre tipos.
- Go by Example: Interfaces — Tutorial prático sobre interfaces em Go com exemplos de implementação implícita.
- Structural vs Nominal Typing in TypeScript — Exemplo interativo no Playground do TypeScript demonstrando a diferença entre tipagem estrutural e nominal.
- Go Specification: Type Identity — Especificação oficial de Go sobre identidade de tipos e regras de compatibilidade nominal.
- Understanding TypeScript Structural Typing — Artigo técnico do LogRocket explicando como a tipagem estrutural funciona no TypeScript com exemplos práticos.