{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"composite": true,
"declaration": true,
"declarationMap": true,
"incremental": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"references": [
{ "path": "../shared-utils" }
]
}Dacă ai lucrat vreodată într-un monorepo cu mai mult de 3-4 pachete, știi deja că TypeScript poate deveni incredibil de leneș. Am avut un caz la fostul job: un proiect cu 12 sub-pachete și în jur de 85k linii de cod, unde build-ul dura aproape 3 minute doar pentru type-checking. Trecerea la project references și configurarea corectă ne-a salvat minute bune în CI și ne-a adus un boost de 40% la viteza de build local.
Capcana clasică: Doar paths în root
Cea mai comună greșeală pe care o văd este maparea tuturor pachetelor prin paths în tsconfig-ul din root. Pare simplu la început. Adaugi "@my-project/shared": ["packages/shared/src"] și totul compilează.
Dar există o problemă majoră cu abordarea asta: TypeScript tratează tot monorepo-ul ca pe un singur proiect uriaș. Când modifici o linie de cod în shared, compilerul reevaluează absolut tot. Dacă ai un serviciu mare care depinde de el, IDE-ul tău o să înceapă să gâfâie, iar consumul de RAM sare lejer de 4GB. Proprietatea paths e gândită pentru alias-uri interne dintr-un singur proiect, nu pentru legat pachete independente în monorepo.
Soluția curată: Composite și Project References
Pentru a rezolva asta, trebuie să spargem proiectul în module independente folosind flag-ul composite: true. Acest flag îi spune lui TS două lucruri:
- Proiectul va produce fișiere
.d.ts(declarații de tipuri) pentru ca alte proiecte să le poată consuma direct, fără să recompileze tot codul sursă de fiecare dată. - Va genera un fișier
.tsbuildinfocare ține evidența cache-ului pentru build-uri incrementale.
Trade-off-ul sincer? Configurația devine mult mai rigidă. Nu mai poți face importuri circulare între pachete (ceea ce e un lucru bun pe termen lung, dar te obligă la refactoring acum) și trebuie să declari explicit fiecare dependență în array-ul de references. Dacă pachetul api folosește shared, trebuie să specifici asta clar în tsconfig.json-ul din api, altfel compilerul va refuza să ruleze.
Cum funcționează rularea incrementală
La root ai un tsconfig.json simplu care doar pointează către sub-proiecte, iar rularea nu se mai face cu tsc, ci cu tsc --build (sau prescurtat tsc -b).
Comanda asta e deșteaptă: analizează graful de dependențe, vede ce s-a schimbat de la ultimul build (folosind acele fișiere .tsbuildinfo) și compilează doar diferențele. Pe proiectul nostru de 85k linii de cod, după o modificare minoră în API, build-ul incremental dura sub 4 secunde, față de 45 de secunde cât dura înainte fără cache.
Singurul minus real e că în pipeline-urile de CI/CD trebuie să ai grijă să salvezi folderul de build cache sau să dai cache la fișierele .tsbuildinfo, altfel build-ul o ia de la capăt pe mașinile virtuale curate din cloud.
Voi cum gestionați asta? Mergeți pe project references sau lăsați bundlerul (Vite, Esbuild, Turbopack) să ignore tipurile la build și faceți type-checking global separat?