type Resource = 'users' | 'invoices' | 'teams';
type Action = 'read' | 'write' | 'delete' | '*';
type Scope = 'own' | 'all';
// Generare automată: "api:users:read:own", "api:invoices:*:all", etc.
export type Permission = `api:${Resource}:${Action}:${Scope}`;
// Type-guard util pentru middleware
export function isValidPermission(perm: string): perm is Permission {
const parts = perm.split(':');
if (parts.length !== 4 || parts[0] !== 'api') return false;
const [, resource, action, scope] = parts;
const resources = ['users', 'invoices', 'teams'];
const actions = ['read', 'write', 'delete', '*'];
const scopes = ['own', 'all'];
return resources.includes(resource) &&
actions.includes(action) &&
scopes.includes(scope);
}Am refăcut anul trecut sistemul de autorizare pentru un SaaS B2B cu vreo 40 de entități în Postgres și 6 roluri distincte. Problema clasică: string-uri aruncate aiurea prin middleware-uri gen "api:users:read" sau "api:user:write" (cu typo stupid între singular și plural) care treceau lejer de code review și crăpau discret la runtime. Soluția elegantă în TypeScript modern nu e un enum gigantic scris de mână, ci template literal types compuse curat.
De ce pică abordarea cu enum-uri clasice
Mulți sar direct la un enum Permission sau un mega-obiect const PERMISSIONS = { ... } as const. La început pare organizat. Dar la 40 de resurse înmulțite cu 5 acțiuni de bază (read, create, update, delete, manage), te trezești cu 200 de linii de cod repetitiv doar ca să mapezi niște stringuri.
Dacă mâine schimbi denumirea unei resurse din teams în workspaces, începe vânătoarea prin fișiere. Cu template literals, definești doar vocabularul de bază:
- Resursele disponibile (ex:
'users' | 'invoices' | 'workspaces') - Acțiunile permise (ex:
'read' | 'write' | 'delete') - Nivelul de granularitate (ex:
'own' | 'all')
TypeScript face produsul cartezian automat. Compilatorul îți generează pe loc toate combinațiile valide, fără să scrii tu manual nici măcar un singur string compus. Primești autocomplete nativ în IDE și zero șanse de typo.
Cazul real: wildcard-uri și ierarhii
În practică, permisiunile rareori se opresc la o simplă pereche resursă-acțiune. Ai aproape mereu nevoie de permisiuni administrative cu wildcard, cum ar fi "api:invoices:*" sau chiar "api:*:*" pentru super-admini.
Poți extinde union-ul adăugând '*' în lista de acțiuni sau resurse. Mai mult, poți scrie un mic helper type generic care verifică dacă un set de permisiuni primite de la utilizator satisface o permisiune cerută de un endpoint. În codul de mai jos am lăsat o structură minimalistă pe care o folosim la protejarea rutelor în Fastify.
Trade-off-ul: atenție la explozia combinatorică
Merge brici pentru claritate și DX, dar are un cost ascuns dacă te ia valul. Am pățit pe un monorepo mare să combinăm patru union-uri diferite într-un template literal, plus o mică recursivitate pentru roluri moștenite.
Ce s-a întâmplat? TypeScript a încercat să calculeze un union de vreo 45.000 de stringuri posibile. Language serverul din VS Code a înghețat complet, dădea crash la fiecare restart, iar build-ul de CI a sărit de la 40 de secunde la peste 2 minute și jumătate.
Regula mea de bun simț: dacă produsul cartezian trece de 1.000-2.000 de variante, oprește-te. În situația aia, e mult mai sănătos să folosești un string simplu validat la runtime prin Zod cu un branded type (type Permission = string & { readonly __brand: unique symbol }), decât să forțezi compilatorul să devină motor de calcul simbolic.
Voi cum gestionați permisiunile granulare în backend? Mergeți pe tipuri stricte derivate în TS sau externalizați verificările către biblioteci gen CASL ori direct prin politici în Postgres?