type Resource = 'users' | 'billing' | 'projects';
type Action = 'read' | 'write' | 'delete';
export type Permission = `api:${Resource}:${Action}`;
type ExtractResource<T extends Permission> = T extends `api:${infer R}:${Action}` ? R : never;
function checkPermission<P extends Permission>(userPerms: P[], required: P): boolean {
return userPerms.includes(required);
}
const myPerms: Permission[] = ['api:users:read', 'api:billing:write'];
// Funcționează perfect:
checkPermission(myPerms, 'api:users:read');
// Eroare la compilare: Type '"api:user:read"' is not assignable to type 'Permission'
// checkPermission(myPerms, 'api:user:read');M-am lovit acum vreo 6 luni de un bug stupid în producție: un coleg pusese din greșeală api:user:delete în loc de api:users:delete în decoratorul de verificare al unui endpoint. Nimic n-a crăpat la build, totul arăta curat în PR, dar permisiunea nu se potrivea niciodată cu ce aveam salvat în baza de date. Am trecut tot sistemul de RBAC pe Template Literal Types și am eliminat instant o întreagă clasă de erori.
Dacă lucrezi la un SaaS unde ai zeci de roluri și resurse, string-urile libere pentru permisiuni sunt o bombă cu ceas. TypeScript 4.1 a introdus posibilitatea de a manipula string-uri direct la nivel de tip, iar combinația asta schimbă complet jocul pentru DX.
Constructia tipului de la simplu la avansat
Să presupunem că ai câteva resurse principale și o listă fixă de acțiuni. În loc să scrii un enum uriaș sau să accepți string peste tot, definești ununions curate:
type Resource = 'users' | 'billing' | 'projects';
type Action = 'read' | 'write' | 'delete';
Prin sintaxa de template string, TypeScript va genera automat produsul cartezian între cele două uniuini. Adică type Permission = api:${Resource}:${Action}; nu e un simplu string, ci devine automat o uniune de 9 string-uri literale posibile (api:users:read, api:users:write, etc.).
Ce e și mai tare e că poți folosi keyword-ul infer pentru a deconstrui aceste tipuri în interiorul middleware-urilor tale. Dacă vrei o funcție care primește un string de permisiune și îți extrage doar resursa pentru logare sau audit, o poți tipa la milimetru fără să faci casting la any sau să spargi aplicația în runtime.
Trade-off-ul real: Timpul de compilare (tsc latency)
Tehnica asta e genială pentru autocompletion în IDE și previne greșelile de tipar, dar vine cu un cost pe care puțină lume îl menționează: exponențialitatea combinărilor.
La un proiect cu vreo 40 de microservicii unde aveam 15 resurse, 8 acțiuni și 4 medii de executare (sys:${Env}:${Resource}:${Action}), numărul de combinări din tip a sărit de la câteva zeci la peste 2.000 de variante. Rezultatul? Serverul de Language Tools din VS Code a început să gâfâie, iar timp de build la tsc --noEmit a crescut de la 3.2 secunde la aproape 14 secunde.
Soluția pe care am aplicat-o a fost să scindăm tipul global în sub-domenii delimitate strict per modul, în loc să avem un Monolithic Permission Type importat peste tot în aplicație. Am economisit cam 60% din timpul de compilare făcând doar optimizarea asta.
Concluzie
Template Literal Types sunt alegerea perfectă când vrei să impui convenții stricte de denumire fără să adaugi biblioteci de validare în bundle-ul de runtime. Cât timp păstrezi unghiul de combinări sub câteva sute de variante, DX-ul câștigat merită fiecare secundă investită.
Voi cum gestionați permisiunile în proiectele mari? Folosiți tipare de genul ăsta sau mergeți pe enume clasice și validare la runtime cu Zod?