type Resource = 'users' | 'products' | 'billing';
type Action = 'read' | 'write' | 'delete';
// Generăm automat 'users:read' | 'users:write' | ...
type Permission = `${Resource}:${Action}`;
// Restricționăm combinații specifice (ex: billing nu are delete)
type ValidPermission = Permission extends 'billing:delete' ? never : Permission;
function checkAccess(permission: ValidPermission) {
// logică de verificare
}
// TS Error: Argument of type '"billing:delete"' is not assignable...
// checkAccess('billing:delete');
// Autocomplete perfect & compilează curat:
checkAccess('users:write');Am refăcut recent sistemul de autorizare la un SaaS B2B unde aveam peste 140 de endpoint-uri și vreo 8 roluri dinamice. Trecerea de la string-uri simple la template literal types ne-a salvat de la zeci de typos în PR-uri și ne-a dat autocomplete instant în IDE. Dacă lucrezi pe aplicații mari unde permisiunile sunt exprimate ca resource:action sau scope:resource:action, trucul ăsta îți schimbă complet modul în care scrii codul.
De ce ne încurcăm în string-uri simple
Până acum ceva timp, aveam un fișier uriaș de constante sau un enum chior. În cel mai bun caz, cineva făcea un union tip type Permission = 'users:read' | 'users:write' | 'billing:read'. Problema apare când aplicația crește. Ai 20 de resurse și 5 acțiuni posibile pe fiecare resursă. Să le scrii de mână înseamnă 100 de variante, iar refactoring-ul devine un coșmar pur.
Dacă puneai doar string ca tip la funcția hasPermission(permission: string), pierdeai orice siguranță. Oricine putea să treacă 'user:read' în loc de 'users:read' și aflai abia în producție că verificarea returnează mereu false.
Construcția tipurilor dinamice
TypeScript ne permite să combinăm union-uri folosind sintaxa de template literals, exact ca în JavaScript. Dacă ai o listă de resurse și o listă de acțiuni, compiler-ul va genera automat produsul cartezian dintre ele.
Partea frumoasă e că poți adăuga restricții mai avansate. De exemplu, poate resursa audit_logs suportă doar acțiunea read, în timp ce users le suportă pe toate. Poți folosi helper types sau Exclude ca să elimini combinațiile invalide din tipul final.
Am aplicat chestia asta într-un proiect cu 8k utilizatori activi lunar și am eliminat complet bug-urile legate de denumiri greșite în frontend când verificam drepturile pe UI (de exemplu <Can do="users:delete">).
Trade-off-ul de care nu-ți spune nimeni: TypeScript OOM
Template literal types sunt extrem de puternice, dar au o capcană mare: explozia combinatorie. Dacă încerci să combini 4 union-uri de câte 10 elemente fiecare (ex: env:tenant:resource:action), TypeScript trebuie să creeze în memorie 10.000 de tipuri de string-uri literale.
Am pățit-o la un build de CI/CD unde tsc a rămas fără memorie (JavaScript heap out of memory) pentru că un coleg generase un tip cu peste 15.000 de combinații teoretice. TypeScript nu le procesează lazy, ci le evaluează pe toate deodată.
Regula mea de aur: dacă tipul tău generat depășește ~500-1000 de variante, regândește structura sau folosește discriminated unions în loc de un singur string gigant.
Unde merită folosit și unde nu
Merge impecabil pentru:
- Rutare de API (
/api/v1/${Resource}) - Permisiuni RBAC / ABAC (
${Resource}:${Action}) - Formatare de evenimente în Event-Driven Architecture (
user:created,order:shipped)
Evită-l dacă permisiunile vin pur dinamic dintr-un backend fără o schemă fixă sau dacă lista de resurse e atât de mare încât dă peste cap serverul de Language Support din VSCode.
Voi cum gestionați permisiunile în TypeScript? Le generați automat din schema OpenAPI/Prisma sau încă le scrieți de mână?