eduardweb.
TypeScript avansatAvansat#architecture#typescript#type-safety#rbac

Permisiuni type-safe în TypeScript: De la stringuri oarbe la Template Literal Types

De Ioana Marinescu, 23 aug. 2026 · 21 vizualizări · 3 like-uri

Postat 23 aug. 2026
typescript
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?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.