type Resource = "users" | "posts" | "billing";
type Action = "create" | "read" | "update" | "delete";
// Combinăm dinamic tipurile
type Permission = `api:${Resource}:${Action}`;
// Helper pentru verificare type-safe
function hasPermission(userPerms: Permission[], required: Permission): boolean {
return userPerms.includes(required);
}
// Exemplu de utilizare corectă
const userPermissions: Permission[] = ["api:users:read", "api:posts:create"];
hasPermission(userPermissions, "api:users:read"); // OK
// @ts-expect-error - Va da eroare de compilare (typo la user)
hasPermission(userPermissions, "api:user:read");Am avut recent de implementat un sistem de RBAC (Role-Based Access Control) destul de stufos la un proiect cu vreo 12.000 de utilizatori activi. La început, totul era simplu: aveam câteva string-uri chioare aruncate prin baza de date și prin cod. "users:write", "reports:read", chestii clasice.
Problemele au apărut când echipa a crescut la 8 developeri. Cineva a scris "user:write" (la singular) în loc de "users:write" într-un middleware de rute. Codul a trecut de code review, a trecut de teste (că na, testam doar happy path-ul pe rutele mari) și a ajuns fericit în producție. Rezultatul? Un ecran alb și clienți nervoși timp de două ore până ne-am prins de typo.
Atunci mi-am zis că e momentul să las lenea și să folosesc template literal types din TypeScript. Chestia asta e disponibilă încă din versiunea 4.1, dar mulți o evită pentru că pare prea "academică" la prima vedere. În realitate, salvează vieți.
Cum definim matricea de permisiuni
Ideea e simplă. În loc să scrii manual o listă gigantică de string-uri în vreun enum sau tip unitar, lași compilatorul să facă munca grea de combinare.
Definim mai întâi domeniile (resursele) și acțiunile posibile ca tipuri union simple. TypeScript ne permite să le concatenăm folosind o sintaxă identică cu cea a template literal-urilor din JavaScript, doar că la nivel de tipuri.
Când scrii api:${Resource}:${Action}, TypeScript generează automat un union cu toate combinațiile posibile. Dacă ai 5 resurse și 4 acțiuni, obții instant un set de 20 de string-uri valide pe care compilatorul le va valida la sânge peste tot în cod.
Unde lovește realitatea: Trade-off-urile
Nimic nu e gratis în inginerie. Am observat rapid că această abordare are și câteva părți mai puțin plăcute pe care trebuie să le iei în calcul înainte să rescrii tot codebase-ul.
În primul rând, performanța compilatorului. Dacă ai un proiect gigant, cu peste 80 de resurse și 10 acțiuni posibile, TypeScript va genera în spate o matrice de 800 de tipuri. VS Code va începe să agațe la autocomplete, iar timpul de build va crește vizibil (la noi a crescut cu vreo 4 secunde la rularea de development, ceea ce e destul de enervant la hot-reloading). Limita hard în TypeScript pentru tipurile generate prin template literals este de 10.000 de instanțe. Dacă o depășești, compilatorul pur și simplu va refuza să ruleze.
În al doilea rând, parsarea dinamică. Când primești permisiunile de la un API extern (de exemplu, sub formă de array de string-uri din JWT), tot trebuie să faci type assertion (as Permission[]) sau să scrii un type guard destul de plictisitor ca să convingi compilatorul că string-ul ăla din JSON e sigur. Template literal types te ajută enorm la nivel de aplicație (in-app type safety), dar granița cu exteriorul (I/O) rămâne la fel de nesigură dacă nu ești atent.
Concluzia mea
Pentru noi, trecerea la template literals a eliminat complet clasa de bug-uri de tip "typo în permisiuni". Am economisit zeci de ore de debugging și frustrare. Dacă ai un sistem de permisiuni mediu spre mare, merită efortul de refactoring din prima zi.
Voi cum gestionați permisiunile în TypeScript? Mergeți pe string-uri simple, template literals sau folosiți biblioteci externe gen CASL?