eduardweb.
TypeScript avansatAvansat#typescript#backend#securitate#webdev

Permisiuni type-safe cu Template Literal Types: fără typo-uri în RBAC

De Bogdan Răducanu, 12 oct. 2026 · 9 vizualizări · 3 like-uri

Postat acum 4 zile
typescript
type Resource = 'users' | 'invoices' | 'teams';
type Action = 'read' | 'write' | 'delete';
type Scope = 'all' | 'own';

export type Permission = `api:${Resource}:${Action}:${Scope}` | `api:${Resource}:*`;

type ParsePermission<T extends string> = 
  T extends `api:${infer R extends Resource}:${infer A extends Action}:${infer S extends Scope}`
    ? { resource: R; action: A; scope: S }
    : null;

export function checkPermission<P extends Permission>(
  required: P,
  userPermissions: Permission[]
): boolean {
  if (userPermissions.includes(required)) return true;
  
  const [_, resource] = required.split(':');
  return userPermissions.includes(`api:${resource as Resource}:*`);
}

Am pierdut acum doi ani aproape o zi întreagă căutând un bug stupid într-un microserviciu cu vreo 40 de tabele și roluri granulare. Un coleg scrisese users:udpate în loc de users:update într-un guard de rută, testul de integrare a picat silențios prin fallback-ul de deny, iar TypeScript nu a zis nimic fiindcă parametrul funcției era tastat generic ca string.

Dacă ai un sistem RBAC sau ABAC unde verifici permisiuni manual, tiparea lor strictă nu e opțională. Template literal types, apărute încă din TS 4.1, rezolvă problema asta elegant fără să fii nevoit să ții manual un enum de 300 de rânduri.

Produsul cartezian automat

Magia template literal types constă în modul în care TypeScript combină union types. Când pui două uniuni într-un template string, compilatorul generează automat produsul cartezian al tuturor variantelor posibile.

Dacă definești resursele tale de bază și acțiunile CRUD permise, le concatenezi într-o singură linie de cod. Obții instant un set complet de string-uri permise, iar IDE-ul îți va da autocomplete exact când scrii api:users:create sau api:invoices:read. Dacă greșești o singură literă, compilatorul refuză să facă build-ul.

Extragerea valorilor la runtime cu infer

Definirea tipului e doar prima jumătate a ecuației. Partea interesantă apare când vrei să scrii o funcție utilitară care validează permisiunea și extrage automat componentele din string, fără regex-uri fragile sau split(':') care returnează string[] chior.

Folosind infer, poți forța TypeScript să desfacă string-ul înapoi în bucăți tipate. Compilatorul știe exact că primul element după api: este o resursă validă, iar ce urmează după al doilea separator este o acțiune. Astfel poți construi middleware-uri de autorizare în care contextul de securitate primește obiecte structurate, nu string-uri primitive.

Unde începe să doară: limitările de memorie

Sună ideal până când sari calul. La proiectul de care vorbeam, am fost tentați să facem produs cartezian între 45 de resurse, 6 acțiuni, 3 scope-uri (own, team, global) și 4 medii de execuție.

Rezultatul? Peste 3.000 de combinații într-o singură uniune. TypeScript a început să gâfâie vizibil: timpul de tsc --noEmit a crescut de la 4 secunde la 11 secunde, iar Language Server-ul din VS Code crăpa aleator când încerca să ofere sugestii de autocomplete.

Trade-off-ul e simplu: ține uniunile plate și cât mai mici. Dacă ai nevoie de permisiuni extrem de dinamice sau wildcard-uri complexe pe mai multe niveluri (gen api:tenant:*:billing:*), nu forța compilatorul să le genereze pe toate anticipat. Folosește tipuri deschise precum `api:${string}` pentru segmente dinamice și păstrează verificările stricte doar pentru resursele critice.

Voi cum gestionați permisiunile în codebase-uri mari: mergeți pe string-uri validate cu Zod la runtime sau vă bazați 100% pe sistemul de tipuri din TS?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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