type Resource = "user" | "billing" | "project";
type Action = "create" | "read" | "update" | "delete";
// TypeScript generează automat toate combinațiile posibile
type Permission = `api:${Resource}:${Action}`;
interface User {
id: string;
permissions: Permission[];
}
function hasAccess(user: User, requiredPermission: Permission): boolean {
return user.permissions.includes(requiredPermission);
}
const dev: User = {
id: "usr_102",
permissions: ["api:user:read", "api:project:create"]
};
// Compiles perfectly:
hasAccess(dev, "api:project:create");
// Error: Argument of type '"api:billing:destroy"' is not assignable...
// hasAccess(dev, "api:billing:destroy");Salutare. Am lucrat recent la refactorizarea unui sistem de permisiuni pentru o aplicație SaaS cu peste 14.000 de utilizatori activi. Ne-am lovit de clasica problemă: cum naiba facem să nu scăpăm string-uri greșite prin cod când verificăm drepturile de acces, fără să scriem mii de linii de boilerplate?
Înainte, foloseam un enum gigant sau obiecte imbricate care deveneau rapid un coșmar de întreținut. Dacă adăugai o resursă nouă, trebuia să modifici în cinci locuri diferite și mereu exista riscul ca cineva să scrie greșit o cheie. Soluția curată pe care am aplicat-o folosește template literal types, o funcționalitate din TypeScript care ne-a salvat de la zeci de bug-uri potențiale la runtime și ne-a oferit autocomplete instant în IDE.
Cum funcționează magia?
Ideea e simplă. TypeScript ne permite să folosim sintaxa de template literals din JavaScript direct în sistemul de tipuri. Practic, putem genera tipuri noi prin concatenarea altor tipuri de tip string union.
Să zicem că avem resursele noastre de bază și acțiunile standard din CRUD. În loc să scriem manual fiecare combinație posibilă de tip api:user:create, api:user:read și tot așa, lăsăm compilerul să facă înmulțirea carteziană pentru noi.
Când scrii api:${Resource}:${Action}, TypeScript generează automat un union cu toate permutările posibile. Dacă ai 4 resurse și 4 acțiuni, ai instant 16 tipuri stricte, fără să scrii decât două linii de cod. Dacă cineva încearcă să verifice o permisiune inexistentă, cum ar fi api:billing:destroy (în loc de delete), compilerul va urla imediat.
Trade-off-ul de care nu-ți spune nimeni
Sună perfect, nu? Ei bine, vine cu un cost destul de clar pe care l-am simțit pe pielea mea.
Dacă ai o aplicație enterprise imensă, cu 80 de resurse și vreo 10 acțiuni custom, TypeScript va genera 800 de tipuri unice doar din acea linie. Dacă începi să faci și manipulări recursive pe ele sau să le folosești în alte tipuri mapate complexe, performanța compilerului scade dramatic. La noi, pe un proiect mai vechi, timpul de build local crescuse cu vreo 12 secunde din cauza tipurilor mult prea inteligente, iar autocomplete-ul în VS Code începuse să aibă un lag sesizabil.
Sfatul meu după ce am implementat asta în producție: folosește-le cu cap. Dacă ai un set limitat și clar de resurse, este soluția ideală. Dacă ai sute de permisiuni extrem de dinamice care vin din baza de date la runtime și se schimbă des, s-ar putea ca abordarea asta extrem de strictă la compile-time să devină mai mult o povară decât un ajutor. Noi am rezolvat-o prin împărțirea permisiunilor pe module mai mici, evitând un singur tip global uriaș.
Cum gestionați voi permisiunile în proiectele mari de TS? Mergeți pe flexibilitate la runtime cu string-uri simple sau preferați siguranța asta strictă, chiar dacă mai încetinește compilerul?