TypeScript com Prisma: inferência de tipos do banco

1. Introdução ao Prisma e sua integração com TypeScript

Prisma é um ORM moderno para Node.js e TypeScript que revoluciona a forma como desenvolvedores interagem com bancos de dados. Sua principal vantagem é a geração automática de tipos TypeScript a partir do schema do banco, eliminando a necessidade de escrever interfaces manualmente e reduzindo drasticamente erros em tempo de execução.

O fluxo de trabalho com Prisma segue três etapas fundamentais: definição do schema → execução de migrações → geração do cliente tipado. Esse ciclo garante que o código TypeScript esteja sempre sincronizado com a estrutura real do banco de dados.

2. Configuração inicial do Prisma em um projeto TypeScript

Para iniciar, instale as dependências necessárias:

npm install prisma @prisma/client
npm install -D ts-node @types/node

Crie o arquivo schema.prisma na raiz do projeto:

// schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

Gere o cliente Prisma:

npx prisma generate

Configure o tsconfig.json para suportar módulos modernos:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

3. Modelagem de dados e tipos gerados automaticamente

Ao definir modelos no schema, o Prisma gera automaticamente interfaces TypeScript correspondentes. Por exemplo, para o modelo User, o Prisma cria:

// Gerado automaticamente pelo Prisma
interface User {
  id: number;
  email: string;
  name: string | null;
  posts: Post[];
}

Você pode modelar relacionamentos complexos:

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  comments  Comment[]
}

model Comment {
  id      Int    @id @default(autoincrement())
  text    String
  post    Post   @relation(fields: [postId], references: [id])
  postId  Int
}

4. Inferência de tipos nas consultas CRUD

O Prisma infere tipos automaticamente em todas as operações CRUD:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Retorno totalmente tipado
const users = await prisma.user.findMany();
// users: User[]

const user = await prisma.user.findUnique({
  where: { id: 1 }
});
// user: User | null

// Seleção de campos específicos
const userEmail = await prisma.user.findUnique({
  where: { id: 1 },
  select: { email: true, name: true }
});
// userEmail: { email: string; name: string | null } | null

Use Prisma.PromiseReturnType para tipar funções que encapsulam consultas:

import { Prisma } from '@prisma/client';

async function getUserWithPosts(userId: number) {
  return prisma.user.findUnique({
    where: { id: userId },
    include: { posts: true }
  });
}

type GetUserWithPostsResult = Prisma.PromiseReturnType<typeof getUserWithPosts>;
// GetUserWithPostsResult: User & { posts: Post[] } | null

5. Tipagem de filtros, ordenação e paginação

Todos os parâmetros de consulta são fortemente tipados:

// Filtros tipados
const activeUsers = await prisma.user.findMany({
  where: {
    email: { contains: "@example.com" },
    posts: { some: { published: true } }
  },
  orderBy: { name: 'asc' },
  take: 10,
  skip: 0
});

// Tipos de filtros disponíveis
type UserWhereInput = Prisma.UserWhereInput;
type UserOrderByInput = Prisma.UserOrderByWithRelationInput;

// Paginação com cursor
const paginatedUsers = await prisma.user.findMany({
  take: 10,
  cursor: { id: lastUserId },
  skip: 1, // Skip the cursor
  orderBy: { id: 'asc' }
});

6. Relacionamentos e joins tipados

O Prisma gera tipos específicos para relações, garantindo segurança total:

// Incluir relações tipadas
const userWithRelations = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        comments: true
      }
    }
  }
});
// userWithRelations: User & { posts: (Post & { comments: Comment[] })[] } | null

// Tratamento de null em relações opcionais
if (userWithRelations) {
  const postTitles = userWithRelations.posts.map(post => post.title);
  // postTitles: string[]
}

// Relações opcionais
const postWithOptionalAuthor = await prisma.post.findUnique({
  where: { id: 1 },
  include: { author: true }
});
// author pode ser null se não existir

7. Transações e operações em lote com tipos

Transações mantêm a tipagem completa:

// Transação tipada
const [user, post] = await prisma.$transaction([
  prisma.user.create({
    data: { email: "user@example.com", name: "John" }
  }),
  prisma.post.create({
    data: { title: "First Post", authorId: 1 }
  })
]);
// user: User, post: Post

// Operações em lote
const createdUsers = await prisma.user.createMany({
  data: [
    { email: "user1@example.com", name: "User 1" },
    { email: "user2@example.com", name: "User 2" }
  ]
});
// createdUsers: { count: number }

const updatedPosts = await prisma.post.updateMany({
  where: { published: false },
  data: { published: true }
});
// updatedPosts: { count: number }

8. Boas práticas e dicas de manutenção

Para manter a tipagem segura e o código sustentável:

// Evitar any - usar tipos gerados
// ❌ Ruim
function processUser(user: any) {
  return user.email;
}

// ✅ Bom
function processUser(user: Prisma.UserGetPayload<{}>) {
  return user.email;
}

// Validação em runtime com Prisma.Validator
import { Prisma } from '@prisma/client';

const userData: Prisma.UserCreateInput = {
  email: "test@example.com",
  name: "Test User"
};

// Versionamento com migrações
// npx prisma migrate dev --name add_user_role

// Lidar com mudanças sem quebrar tipagem
// Sempre executar npx prisma generate após alterações no schema

Dicas importantes:
- Execute npx prisma generate sempre que modificar o schema
- Use Prisma.UserGetPayload para tipos parciais
- Mantenha as migrações versionadas no controle de versão
- Utilize Prisma.UserCreateInput e Prisma.UserUpdateInput para validação de dados

Referências