type Resource = 'users' | 'posts' | 'billing';
type Action = 'create' | 'read' | 'update' | 'delete';
// TypeScript generează automat: "api:users:create" | "api:users:read" | ...
type ApiPermission = `api:${Resource}:${Action}`;
// Putem extinde cu wildcard-uri pentru administratori
type AdminPermission = `admin:${Resource}:*`;
type AppPermission = ApiPermission | AdminPermission;
function checkAccess(userPermissions: AppPermission[], required: AppPermission): boolean {
return userPermissions.includes(required);
}
// Execuție corectă cu Autocomplete în IDE:
checkAccess(['api:users:read'], 'api:users:read');
// Eroare de compilare: Argument of type '"api:users:write"' is not assignable to parameter of type 'AppPermission'
// checkAccess(['api:users:read'], 'api:users:write');Anul trecut am refăcut sistemul de RBAC la o platformă B2B unde aveam în jur de 15.000 de request-uri pe minut. Cea mai mare durere de cap? String-urile hardcodate pentru permisiuni, presărate prin tot codul. O greșeală de tipar precum user:read în loc de users:read trecea liniștită de linter, dar ne rupea aplicația în producție.
De la magic strings la autocomplete în IDE
Înainte aveam un fișier uriaș cu constante sau, mai rău, string-uri scrise de mână direct în decoratori de tipul @HasPermission('user:read'). Când am refactorizat modulul de facturare, ne-am trezit cu 12 tichete în Jira în prima oră după deploy pentru că jumătate din frontend folosea versiunea veche a string-ului.
Cu Template Literal Types poți să forțezi compiler-ul să construiască tipul pe baza domeniului tău. Definești resursele de bază și acțiunile, iar TypeScript generează automat o uniune strictă de string-uri permise.
Dacă încerci să pasezi api:billing:execute, IDE-ul îți urlă imediat roșu și îți oferă autocompletion doar cu combinațiile valide. Câștigul a fost uriaș: am eliminat complet bug-urile legate de typos pe rutele de securitate și am salvat zeci de ore la refactoring.
Trade-off-ul dureros: Explozia combinatorie
Aici vine chestia despre care nu prea se vorbește în tutorialele entuziasmate. Permisiunile type-safe sunt fantastice până când generezi prea multe tipuri în memorie.
La un moment dat am vrut să fim deștepți și am adăugat în template și mediul (dev | staging | prod), rolul, plus resursele și acțiunile imbricate. Rezultatul? O matrice care a generat peste 120.000 de combinații posibile.
Când rula tsc, procesorul urca în 100%, iar build-ul pe CI a crescut de la 45 de secunde la aproape 4 minute, până când TypeScript arunca faimosul Type instantiation is excessively deep and possibly infinite. Compiler-ul pur și simplu a cedat.
Am învățat lecția rapid:
- Nu combina mai mult de 2-3 tipuri generice complexe în același template literal.
- Dacă ai resurse imbricate dinamic, folosește un parser de runtime (precum Zod) și păstrează tipurile TS la un nivel mai plat.
Pattern-ul simplu care funcționează
În loc să faci tipuri infinit de flexibile, e mult mai bine să le spargi pe domenii clare (de exemplu, permisiuni de API vs. permisiuni de UI). Așa păstrezi compilatorul rapid și codul lizibil.
În exemplul de mai jos am lăsat varianta pe care o folosim acum în producție pe un codebase de peste 80k de linii. Funcționează brici și nu încetinește deloc build-ul.
Voi ce abordare folosiți pentru RBAC în TypeScript? Mergeți pe enume-uri clasice, validare la runtime sau lăsați tipurile să facă toată munca?