Erros comuns e como evitá-los

1. Erros com any implícito e explícito

O any é o maior vilão do TypeScript. Ele desativa completamente a verificação de tipos, transformando seu código em JavaScript sem segurança.

// ❌ Ruim: any implícito
function processData(data) {
  return data.name; // Erro: 'data' é implicitamente 'any'
}

// ✅ Correto: ative noImplicitAny no tsconfig.json
// "noImplicitAny": true (ou "strict": true)

// ❌ Ruim: as any explícito
const user = JSON.parse('{"name": "João"}') as any;
console.log(user.email); // Sem erro de compilação, mas runtime crash

// ✅ Correto: use unknown com type guards
const userData: unknown = JSON.parse('{"name": "João"}');

if (typeof userData === 'object' && userData !== null && 'name' in userData) {
  const user = userData as { name: string };
  console.log(user.name);
}

// ✅ Alternativa: use zod ou io-ts para validação runtime
import { z } from 'zod';

const UserSchema = z.object({ name: z.string() });
const parsedUser = UserSchema.parse(JSON.parse('{"name": "João"}'));

2. Problemas com tipos null e undefined

O erro "Object is possibly 'null'" é um dos mais comuns em TypeScript.

// ❌ Ruim: sem strictNullChecks
function getLength(str: string | null) {
  return str.length; // Pode explodir se str for null
}

// ✅ Correto: ative strictNullChecks
function getLength(str: string | null) {
  // Opção 1: early return
  if (str === null) return 0;

  // Opção 2: optional chaining
  return str?.length ?? 0;

  // Opção 3: nullish coalescing
  return (str ?? '').length;
}

// ❌ Ruim: uso excessivo de non-null assertion (!)
const element = document.getElementById('root')!;
element.innerHTML = 'Hello'; // Perigoso se o elemento não existir

// ✅ Correto: verificação adequada
const element = document.getElementById('root');
if (element) {
  element.innerHTML = 'Hello';
}

3. Erros de tipo em arrays e objetos

Mutabilidade inesperada e tipagem inadequada são fontes comuns de bugs.

// ❌ Ruim: array mutável exposto
class UserRepository {
  private users: Array<{ id: number; name: string }> = [];

  getUsers() {
    return this.users; // Permite mutação externa
  }
}

// ✅ Correto: ReadonlyArray
class UserRepository {
  private users: Array<{ id: number; name: string }> = [];

  getUsers(): ReadonlyArray<{ id: number; name: string }> {
    return [...this.users]; // Cópia defensiva
  }
}

// ❌ Ruim: Object.keys com tipagem inadequada
const user = { name: 'Ana', age: 30 } as const;
Object.keys(user).forEach(key => {
  console.log(user[key]); // Erro: key é string, não 'name' | 'age'
});

// ✅ Correto: type assertion segura
(Object.keys(user) as Array<keyof typeof user>).forEach(key => {
  console.log(user[key]); // Funciona
});

// ❌ Ruim: tupla sem as const
const position = [10, 20]; // type: number[]
const x = position[0]; // number, não 10

// ✅ Correto: as const para tuplas literais
const position = [10, 20] as const; // type: readonly [10, 20]

4. Problemas com funções e overloads

Overloads mal definidos e perda de contexto com this são armadilhas frequentes.

// ❌ Ruim: overloads em ordem incorreta
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  return String(value);
}

// ✅ Correto: overloads do mais específico para o mais genérico
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  return String(value);
}

// ❌ Ruim: perda de contexto com this
class Button {
  text = 'Click me';

  onClick() {
    console.log(this.text); // this pode ser undefined
  }
}

const button = new Button();
setTimeout(button.onClick, 1000); // Erro: this é undefined

// ✅ Correto: arrow function ou bind
class Button {
  text = 'Click me';

  onClick = () => { // Arrow function preserva this
    console.log(this.text);
  }
}

// ✅ Alternativa: bind
setTimeout(button.onClick.bind(button), 1000);

// ❌ Ruim: Function como tipo
function executeCallback(cb: Function) {
  cb(1, 2); // Sem segurança de tipos
}

// ✅ Correto: interface de callback
interface Callback {
  (a: number, b: number): number;
}

function executeCallback(cb: Callback) {
  return cb(1, 2);
}

5. Erros com genéricos e inferência

Genéricos mal dimensionados podem causar inferência falha ou tipos muito restritivos.

// ❌ Ruim: genérico muito amplo
function identity<T>(value: T): T {
  return value;
}

// ✅ Correto: genérico restrito quando necessário
function processArray<T extends { id: number }>(items: T[]): number[] {
  return items.map(item => item.id);
}

// ❌ Ruim: inferência falhando com tipos condicionais
type IsString<T> = T extends string ? true : false;
type Result = IsString<number>; // false

// ✅ Correto: use infer com cuidado
type ElementType<T> = T extends (infer U)[] ? U : never;
type Num = ElementType<number[]>; // number

// ❌ Ruim: const genérico sem necessidade
function createPair<T extends string>(a: T, b: T) {
  return [a, b] as const;
}

// ✅ Correto: inferência automática funciona
function createPair<T>(a: T, b: T) {
  return [a, b] as const;
}

6. Problemas com módulos e importação de tipos

Importações circulares e escolha errada entre import type e import causam problemas.

// ❌ Ruim: importação circular
// user.ts
import { Post } from './post';
export class User {
  posts: Post[];
}

// post.ts
import { User } from './user';
export class Post {
  author: User;
}

// ✅ Correto: interface compartilhada
// types.ts
export interface IUser { posts: IPost[]; }
export interface IPost { author: IUser; }

// ❌ Ruim: import type vs import
import { User } from './user'; // Inclui código desnecessário

// ✅ Correto: use import type para evitar tree-shaking
import type { User } from './user'; // Apenas tipo

// ❌ Ruim: export default em tipos
export default interface User {
  name: string;
}

// ✅ Correto: export named
export interface User {
  name: string;
}

7. Erros com classes e herança

Confundir implements com extends e erros de covariância/contravariância são comuns.

// ❌ Ruim: implements vs extends
interface Animal {
  makeSound(): void;
}

class Dog implements Animal {
  makeSound() {
    console.log('Woof');
  }
}

class Puppy extends Dog { // Herda implementação
  makeSound() {
    console.log('Yip');
  }
}

// ❌ Ruim: covariância incorreta em métodos
class Parent {
  getValue(): number | string {
    return 42;
  }
}

class Child extends Parent {
  getValue(): number { // Retorno mais específico - OK
    return 42;
  }
}

// ❌ Ruim: uso incorreto de private vs #
class OldWay {
  private secret = 'hidden'; // Ainda acessível via eval()
}

class ModernWay {
  #secret = 'hidden'; // Verdadeiramente privado
}

8. Erros com tipos avançados (utility types e mapped types)

Utility types mal aplicados podem causar erros sutis.

// ❌ Ruim: Partial aplicado incorretamente
interface Config {
  url: string;
  timeout: number;
}

function createConfig(config: Partial<Config>) {
  return {
    url: config.url ?? 'default', // Pode ser undefined
    timeout: config.timeout ?? 5000,
  };
}

// ✅ Correto: Required para campos obrigatórios
function createConfig(config: Required<Config>) {
  return config;
}

// ❌ Ruim: Pick com chaves que não existem
type UserKeys = Pick<User, 'name' | 'email'>; // Erro se 'email' não existir

// ✅ Correto: verificação com keyof
type UserKeys = Pick<User, keyof User>;

// ❌ Ruim: Record com chaves literais problemáticas
type StatusMap = Record<'active' | 'inactive', boolean>;
const statuses: StatusMap = {
  active: true,
  inactive: false,
};

Conclusão

Evitar esses erros comuns requer prática e atenção aos detalhes. Use sempre strict: true no tsconfig.json, prefira unknown a any, e mantenha seus tipos o mais específicos possível sem serem restritivos demais. Lembre-se: TypeScript é uma ferramenta para ajudar, não para atrapalhar. Use seus recursos com sabedoria.

Referências