/ Fundamentos
Tipagem
TypeScript estrito, tipos do Drizzle, types/ e validação.
5 min de leitura Editar no GitHub
O RoboStage é escrito inteiramente em TypeScript com strict ligado. Os tipos vêm de três lugares:
- do schema do banco, inferidos pelo Drizzle;
- da pasta
types/, com formatos compartilhados pela interface; - dos próprios módulos de domínio em
utils/, ao lado da regra que usam.
Esta página explica onde fica cada tipo e quais padrões o projeto segue.
Configuração
Trecho relevante do tsconfig.json:
{
"compilerOptions": {
"target": "ES2017",
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"allowJs": true,
"paths": { "@/*": ["./*"] }
}
}| Opção | Efeito no projeto |
|---|---|
strict |
Ativa strictNullChecks, noImplicitAny e as demais checagens estritas. Valores possivelmente nulos precisam ser tratados. |
noEmit |
O TypeScript só verifica. Quem compila é o Next.js (Turbopack). |
resolveJsonModule |
Permite import missions from "@/public/data/missions.json" com tipo inferido. |
paths (@/*) |
Importações absolutas a partir da raiz: @/db/schema, @/utils/stagebook/scope. |
allowJs |
Aceita os scripts .mjs de scripts/. |
A checagem de tipos roda no npm run build. Um erro de tipo quebra a build.
Tipos do banco (Drizzle)
As tabelas definidas em db/schema/ já carregam os tipos das linhas. Cada arquivo exporta pares de tipos inferidos:
// db/schema/users.ts
export type UserRow = typeof users.$inferSelect; // o que vem de um SELECT
export type NewUserRow = typeof users.$inferInsert; // o que um INSERT aceita| Tipo | Uso |
|---|---|
XRow ($inferSelect) |
Resultado de consultas. As colunas notNull() são obrigatórias; as outras, T | null. |
NewXRow ($inferInsert) |
Entrada de db.insert(). Colunas com default ficam opcionais. |
Exemplos no código: UserRow, LeagueRow, UserLeagueInterestRow, MilestoneRow.
Colunas jsonb e textos com valores conhecidos
Por padrão, uma coluna jsonb é unknown. O projeto declara o formato com $type<…>():
// db/schema/notifications.ts
data: jsonb("data").$type<Record<string, unknown>>(),
// db/schema/quickbrick.ts — texto no banco, união de literais no TypeScript
kind: text("kind").$type<DiagramKind>().notNull(),Atenção
$type<T>()só muda o tipo. O banco não valida o conteúdo. Dadosjsonbvindos do cliente precisam passar por uma função de validação ou sanitização antes de gravar, comosanitizeProject()no ArenaBuilder.
Enums do Postgres
Os enums de db/schema/enums.ts (personaTypeEnum, leagueRelationEnum, teamRoleEnum) e testModeEnum viram uniões de strings nos tipos inferidos. Por isso, UserRow["personaType"] é "competidor" | "mentor_tecnico" | "entusiasta" | "organizador" | null.
A pasta types/
types/ reúne os formatos usados por componentes e páginas que não são linhas do banco: estados de editores, DTOs e configurações de ferramentas.
| Arquivo | Conteúdo |
|---|---|
labtest.types.ts |
FieldType (number, boolean, text, select, duration), ModeId, FieldDefinition, TestEntry, TestRecord. |
labels.ts |
LabelableKind, TagDTO, MilestoneDTO, ResourceLabels e a lista LABELABLE_KINDS. |
team-workspace.ts |
Resumos da página da equipe: TeamBoardSummary, TeamLabTestSummary, TeamMemberSummary… |
preferences.ts |
UserPreferences e DEFAULT_PREFERENCES. |
thinklab.types.ts, CanvasType.ts |
Elementos, estilos e estado do quadro do ThinkLab. |
anatix.types.ts |
Frames, métricas e sessões da telemetria. |
EmpathyMap.types.ts, FlashQATypes.ts, TimersType.ts, SharksSimulator.types.ts, MatrizRisco.ts, heatmap.ts, TableAnalytics.ts |
Estado de cada ferramenta. |
folders.ts, navigation.ts |
Pastas e itens de menu. |
web-serial.d.ts |
Declarações da Web Serial API, que o lib.dom do TypeScript ainda não traz. |
Dica
Tipos usados por um único módulo ficam no próprio módulo (ex.:
utils/competitions/obr/types.ts,utils/quickbrick/ArenaBuilder/types.ts). Mova um tipo paratypes/quando dois domínios diferentes passarem a usá-lo.
Padrões usados no projeto
Uniões discriminadas
O escopo do Stagebook é o exemplo central. O campo type decide quais outros campos existem:
// utils/stagebook/scope.ts
export type StagebookScope =
| { type: "personal"; userId: string }
| { type: "team"; teamId: string; userId: string; role: TeamRole };
export type TeamScope = Extract<StagebookScope, { type: "team" }>;Depois de if (scope.type === "team"), o TypeScript sabe que scope.teamId e scope.role existem.
O resultado das Server Actions segue o mesmo padrão:
// utils/stagebook/action-result.ts
export type ActionResult<T = void> =
| { ok: true; data: T }
| { ok: false; error: string };Catálogos com as const e keyof typeof
Listas fixas são escritas uma vez como valor, e o tipo é derivado delas. Assim o tipo nunca diverge do dado:
// utils/competitions/routes.ts
export const FLL_SCORE_SEASONS = ["bioglow", "unearthed", "submerged", "masterpiece"] as const;
// utils/competitions/competicoes.ts → routes.ts
export type Competicao = keyof typeof COMPETICOES; // "fll" | "obr"satisfies
Confere a forma de um objeto sem perder os tipos literais inferidos:
// utils/competitions/navigation.ts
menus: [ /* ... */ ] satisfies NavMenuGroup[],Type guards
Valores que chegam de fora (cookie, banco, formulário) são estreitados com funções value is T:
// utils/stagebook/team-permissions.ts
export function isTeamRole(value: unknown): value is TeamRole {
return typeof value === "string" && (TEAM_ROLES as readonly string[]).includes(value);
}Outros exemplos: isPersonaType(), isConfereAICompetition() e isCompeticao(). Quando o valor é desconhecido, o padrão é falhar fechado: um cargo inválido vira colaborador, sem privilégios.
Record com chave tipada
Matrizes e tabelas de configuração usam Record<UniãoDeChaves, Valor>. Se alguém adicionar um cargo novo a TeamRole, o compilador aponta todos os Record<TeamRole, …> incompletos.
const MATRIX: Record<TeamCapability, readonly TeamRole[]> = { /* ... */ };Entrada não confiável
O TypeScript não existe em tempo de execução. Tudo que vem do navegador (argumentos de Server Actions, corpo de requisições, params, cookies) deve ser tratado como unknown até ser validado.
O projeto não usa uma biblioteca de schema como zod. Ele usa funções pequenas em utils/stagebook/validation.ts, que recebem unknown e devolvem o tipo certo ou lançam ValidationError com uma mensagem em português:
| Função | Devolve |
|---|---|
requireText(value, label, { minLength, maxLength }) |
string limpa (espaços normalizados). |
optionalText(value, maxLength) |
string | null |
requireOneOf(value, opções, label) |
Um dos literais permitidos. |
requireEmail(value) |
string |
requireUuid(value, label) / optionalUuid |
UUID validado. |
requireDate(value, label) / optionalDate |
Date |
Exemplo ilustrativo (as actions reais, como as de dashboard/quickbrick/actions.ts, seguem este formato):
export async function renameBoard(id: unknown, name: unknown) {
return runAction(async () => {
const boardId = requireUuid(id, "quadro");
const title = requireText(name, "o nome do quadro", { maxLength: 80 });
// ... escopo, permissão e gravação
});
}runAction() converte ValidationError e StagebookAuthError em { ok: false, error }. A mensagem chega ao usuário porque, em produção, o Next.js esconde a mensagem de erros lançados por Server Actions.
Tipos de componentes e páginas
- Props de página recebem
paramsesearchParamscomoPromise(veja Arquitetura das rotas). - Componentes cliente declaram as props com um
typeouinterfacelocal. Os dois estilos aparecem no código: funções com props tipadas eReact.FC<Props>. - Ícones em catálogos usam o tipo
LucideIcondelucide-react. - Server Components assíncronos podem ser
async functione retornar JSX diretamente.
Boas práticas
- Derive tipos de valores (
as const,typeof,$inferSelect) em vez de reescrevê-los à mão. - Evite
any. Para dados externos, useunknowne uma função de validação. - Prefira uniões de literais (
"runs" | "custom") astringquando o conjunto é fechado. - Não use
aspara "convencer" o compilador sobre dados vindos de fora. Valide. - Ao adicionar uma coluna, atualize o schema primeiro: os tipos
Rowacompanham sozinhos.
Algo errado ou faltando nesta página? Sugira uma edição.