TypeScript9minutos de leitura

6 Hábitos de TypeScript para Código AI-Friendly

Pedi um refactor simples pro assistente de IA. O diff veio limpo, só que a rota era /user/ e o app usa /users/. O catch só fazia console.log e seguia o fluxo.

O modelo não inventou do nada. Ele preencheu os buracos que os tipos deixaram abertos.

Hoje escrevo TypeScript pensando em duas leitoras: o compilador e quem vai sugerir patch no meu PR. Quanto mais o tipo descreve o que pode existir, menos chute entra no diff.

Seis hábitos que uso no dia a dia.


1. Rotas não são string solta

router.push('/users/' + id) funciona até o dia em que alguém (humano ou IA) escreve /user/.

// ❌ Qualquer string passa. Todo mundo chuta.
function navigateTo(path: string) { ... }

Defina o formato das rotas com template literal types ou um mapa as const:

const ROUTES = {
  HOME: '/',
  USERS: '/users',
  USER_DETAIL: '/users/:id',
} as const;

type AppRoute = typeof ROUTES[keyof typeof ROUTES];

function navigate(route: AppRoute) { /* ... */ }

navigate(ROUTES.USER_DETAIL);

As rotas válidas ficam num lugar só. Quem for editar o arquivo vê a lista inteira antes de sugerir caminho novo.


2. Três booleanos no mesmo componente viram estado impossível

isLoading, isError e isSuccess ao mesmo tempo? O TypeScript não impede. A IA também não, e aí você renderiza dado com spinner por cima.

type DataState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function UserList({ state }: { state: DataState<User[]> }) {
  switch (state.status) {
    case 'loading': return <Spinner />;
    case 'error': return <ErrorMsg error={state.error} />;
    case 'success': return <List data={state.data} />;
  }
}

O switch no status obriga tratar cada caso. data só existe quando faz sentido existir.


3. string não distingue userId de email

Para o compilador, os dois são texto. sendEmail(to: string, from: string) troca a ordem e ninguém reclama.

type Brand<K, T> = K & { __brand: T };

type Email = Brand<string, 'Email'>;
type UserId = Brand<string, 'UserId'>;

function createEmail(value: string): Email {
  if (!value.includes('@')) throw new Error('Email inválido');
  return value as Email;
}

function sendInvite(to: Email, from: UserId) { /* ... */ }

const adminId = 'u-123' as UserId;
const userEmail = createEmail('john@example.com');

sendInvite(userEmail, adminId); // ✅
// sendInvite(adminId, userEmail); // ❌

4. Comentário não substitui validação

// Preço deve ser positivo não impede price: -10 no refactor automático.

type Price = Brand<number, 'Price'>;

function createPrice(value: number): Price {
  if (value < 0) throw new Error('Preço deve ser positivo');
  return value as Price;
}

interface Product {
  name: string;
  price: Price;
}

Quem precisa de Price passa por createPrice. A regra roda na criação, não na revisão de PR.


5. throw some no meio do caminho

Num try/catch, a IA costuma logar e seguir. O tipo do erro some.

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

type FetchError =
  | { type: 'NetworkError' }
  | { type: 'NotFound'; id: string };

async function getUser(id: string): Promise<Result<User, FetchError>> {
  // retorna objetos; não faz throw
}

const result = await getUser('123');
if (!result.ok) {
  switch (result.error.type) {
    case 'NotFound': /* ... */
    case 'NetworkError': /* ... */
  }
}

Os erros possíveis estão na assinatura. O switch fecha o circuito.


6. Dado de API não entra com as User

Branded types ajudam dentro do app. Resposta de rede é outra história: as User mente pro compilador e a IA acredita.

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string) {
  const data = await fetch(`/api/users/${id}`).then((res) => res.json());
  return UserSchema.parse(data);
}

Se parse passou, o tipo corresponde ao que chegou.


Da próxima vez que o diff vier estranho, olho os tipos antes de culpar o modelo. Muitas vezes o contrato estava frouxo demais.