type Resource = 'user' | 'document' | 'billing';
type Action = 'create' | 'read' | 'update' | 'delete';
// Generăm automat uniunea de tipuri: "api:user:create" | "api:user:read" | ...
type Permission = `api:${Resource}:${Action}`;
interface User {
id: string;
permissions: Permission[];
}
function hasPermission(user: User, permission: Permission): boolean {
return user.permissions.includes(permission);
}
const dev: User = {
id: 'usr_123',
permissions: ['api:user:read', 'api:billing:update']
};
// Funcționează perfect:
hasPermission(dev, 'api:user:read');
// Eroare de compilare: Argument of type '"api:user:write"' is not assignable...
// hasPermission(dev, 'api:user:write');Salutare! Azi vreau să vorbim despre cum eliminăm abordarea de tip "stringly-typed" din aplicațiile noastre și cum strângem șurubul pe permisiuni folosind Template Literal Types. Am trecut recent prin asta pe un proiect mărișor și DX-ul (developer experience) s-a îmbunătățit radical de a doua zi.
Problema de la care am plecat
Anul trecut am lucrat la un SaaS destul de stufos, cu vreo 140 de rute și un sistem de autorizare granular bazat pe roluri. Inițial, permisiunile erau doar niște string-uri chioare salvate în baza de date și verificate la runtime. Totul părea în regulă până când un coleg a scris api:user:udpate în loc de update într-un middleware de pe backend. Bug-ul a trecut de code review (că nah, cine vede un typo de două litere la 1 noaptea?) și a ajuns direct în staging, unde un buton important pur și simplu nu se mai randa pentru admini.
Atunci mi-am dat seama că trebuie să lăsăm compilerul de TypeScript să își facă treaba și să ne dea peste mână când greșim o singură literă. Am vrut un sistem care să ne permită să scriem permisiuni sub forma api:${Resource}:${Action}, dar complet type-safe.
Cum implementăm asta curat
TypeScript ne permite să combinăm tipuri de tip string exact cum facem cu template literals în JavaScript. Ideea e simplă: definim uniunile de resurse și acțiuni, iar apoi le interpolăm direct în tipul final.
Partea frumoasă e că TypeScript va genera automat produsul cartezian al tuturor combinărilor posibile. Dacă ai 3 resurse și acțiunile clasice de CRUD, tipul rezultat va accepta exact acele combinații valide. Orice altceva în afară de ele va genera o eroare de compilare direct în IDE, înainte ca codul să ajungă măcar în build-ul de staging.
Trade-off-uri pe care nu le găsești în documentație
Suna perfect, nu? Ei bine, în producție lucrurile vin mereu cu un cost. Iată de ce trebuie să ții cont înainte să muți tot proiectul pe pattern-ul ăsta:
- Performanța compilerului (TSC): Dacă ai 80 de resurse și 10 acțiuni posibile, TypeScript trebuie să genereze și să verifice 800 de string-uri în memorie. Nu e o problemă uriașă la numărul ăsta, dar dacă mai adaugi un nivel de granularitate (de exemplu, scope:
global|tenant|own), ajungi rapid la mii de combinații. La un proiect anterior cu peste 5.000 de permutări, VS Code începuse să aibă un delay vizibil de 2-3 secunde la autocomplete-ul de tipuri. - Problema datelor din baza de date: Tipurile astea sunt excelente pe frontend și în codul de backend, dar când tragi permisiunile utilizatorului din baza de date (unde vin ca un simplu
string[]), tot va trebui să faci o validare la runtime (un type guard sau o schemă Zod) înainte de a face typecast către tipul tău strict. TypeScript te ajută la scriere, dar nu te scapă de validarea input-ului.
Pentru noi, trecerea la template literal types a redus bug-urile legate de permisiuni scrise greșit la zero barat. Merită efortul? Absolut, mai ales dacă vrei să dormi liniștit vinerea după-amiază.
Voi cum gestionați permisiunile în proiectele mari de TypeScript? Mergeți pe string-uri simple validate la runtime sau preferați să puneți compilerul la treabă?