type ResourceMap = {
user: 'create' | 'read' | 'update' | 'delete';
billing: 'view_invoice' | 'charge' | 'refund';
audit: 'export' | 'read';
};
type Scope = 'api' | 'internal';
export type Permission = {
[R in keyof ResourceMap]: `${Scope}:${R}:${ResourceMap[R]}`;
}[keyof ResourceMap];
export function hasPermission(userPerms: Permission[], required: Permission): boolean {
return userPerms.includes(required);
}
// Usage:
hasPermission(['api:user:read'], 'api:user:read'); // Valid
// @ts-expect-error Type '"api:billing:delete"' is not assignable to 'Permission'
hasPermission(['api:billing:refund'], 'api:billing:delete');Dacă ai lucrat pe un proiect mare cu RBAC sau permisiuni granulare, probabil te-ai lovit de stringuri magice împrăștiate prin tot codebase-ul. O literă greșită precum 'users:delete' în loc de 'user:delete' trece fluierând de linter, iar bug-ul ajunge cuminte în producție.
Am pățit treaba asta acum vreo doi ani pe o platformă cu vreo 40 de micro-frontends și peste 140 de permisiuni diferite. Backend-ul schimbase convenția din kebab-case în snake_case pentru două submodule, iar în frontend jumătate din butoanele de acțiune erau ascunse complet fără ca cineva să observe la deployment.
Soluția simplă ar fi să pui totul într-un enum uriaș, dar devine rapid un coșmar de mentenanță și nu scalează când permisiunile urmează pattern-uri dinamice pe scopes (api:billing:charge, internal:audit:export). Aici intervin Template Literal Types.
Generarea permisiunilor dependente de resursă
Secretul nu e doar să lipești stringuri cu template literals (${Resource}:${Action}), pentru că asta generează un produs cartezian: dacă ai 10 resurse și 5 acțiuni, permiți acțiuni invalide precum api:audit:refund. În realitate, pe resursa audit ai doar read sau export, nu refund.
Trebuie să mapezi resursele la acțiunile lor reale printr-un obiect de tip dicționar (ResourceMap), apoi să iterezi peste chei și să extragi union-ul final folosind un index access ([keyof ResourceMap]). Rezultatul este un union strict, unde TypeScript validează combinația exactă dintre scope, resursă și acțiune specifică.
Trade-off-ul: combinatorica ucide TS Server-ul
Template Literal Types sunt extrem de comode, dar au un cost real de memorie. La un moment dat am încercat să compun un tip de forma:
${Environment}:${Tenant}:${Service}:${Resource}:${Action}
Rezultatul? TypeScript a generat intern peste 25.000 de variante de union. În VS Code, autocomplete-ul a început să aibă delay de 3-4 secunde, iar comanda de tsc --noEmit în CI a sărit de la 12 secunde la peste un minut.
Regula mea de aur după experiența aia: păstrează adâncimea template literals la maximum 2-3 segmente dinamice. Dacă ai nevoie de mai multe dimensiuni (ex. adaugi și tenant_id), mai bine transmiți tenant-ul ca parametru separat într-o funcție decât să-l înghesui în string-ul de permisiune.
Merge impecabil dacă vrei să ai type-safety total între backend (care poate genera schema de tipuri automat) și verificările din frontend de tip <Can I="api:user:create">. Nu merge deloc dacă încerci să transformi tot contextul aplicației într-un mega-string.
Voi cum gestionați permisiunile granulare în frontend? Mergeți pe tipuri stricte generate sau vă bazați doar pe contract teste?