Treci la conținutul principal

Namespaces în TypeScript

Un namespace este un container cu nume pentru cod înrudit. Grupează tipuri, funcții și clase sub un singur nume, ca să nu intre în conflict cu alt cod.


Cum funcționează namespace-urile în TypeScript

Namespace-urile din TypeScript (numite cândva „module interne”) îți oferă o modalitate de a organiza codul și de a evita conflictele de nume, punând funcționalitatea înrudită într-un container cu nume.

Ele ajută la structurarea proiectelor mari și țin sub control domeniul de vizibilitate, într-un mod curat și ușor de întreținut.

Concepte cheie:

  • Grupare logică: organizează codul înrudit în containere cu nume
  • Controlul vizibilității: stabilește ce părți din cod se văd din exterior
  • Evitarea coliziunilor de nume: previne conflictele dintre lucruri cu nume asemănătoare
  • Organizarea codului: structurează aplicațiile mari într-o ierarhie

Când să folosești namespace-uri:

  • Pentru organizarea codului în aplicații vechi, de dimensiuni mari
  • Când lucrezi cu biblioteci globale
  • Când migrezi de la proiecte JavaScript mai vechi
  • Când lucrezi cu cod care trebuie să fie disponibil global

Observație: Namespace-urile sunt în continuare complet suportate, dar aplicațiile moderne folosesc de obicei module ES (import/export), pentru că modulele oferă o modularitate mai bună și permit uneltelor de împachetare să elimine codul nefolosit (tree-shaking). Înțelegerea namespace-urilor rămâne totuși utilă pentru întreținerea proiectelor vechi și pentru anumite situații din dezvoltarea de biblioteci.


Sintaxa de bază a unui namespace

Un namespace se definește cu cuvântul cheie namespace, urmat de un bloc. Doar membrii marcați cu export pot fi folosiți din afara blocului; tot restul rămâne privat în interiorul namespace-ului.

Exemplu

Namespace-ul Validation exportă o interfață și două clase, în timp ce lettersRegexp este privat în interiorul lui. În exterior, clasele sunt accesate cu notația cu punct: Validation.LettersValidator.

Prompt
Arată sintaxa de bază a unui namespace, cu membri exportați și membri privați. Declară un namespace Validation care exportă o interfață StringValidator (isValid(s: string): boolean), păstrează un lettersRegexp neexportat (/^[A-Za-z]+$/) și exportă două clase care implementează interfața: LettersValidator (doar litere) și ZipCodeValidator (doar cifre și exact cinci caractere). În afara namespace-ului creează câte un obiect din fiecare prin Validation.<Clasă> și afișează isValid pentru "Hello", "Hello123", "12345" și "1234".
Ieșire: program TypeScript și rezultatul capturat

Funcționalități avansate ale namespace-urilor

Namespace-urile pot fi imbricate unul în altul pentru a construi o ierarhie, iar căile lungi de namespace pot fi scurtate cu ajutorul aliasurilor.

Namespace-uri imbricate

App conține două namespace-uri interne exportate, Utils și Models. Membrii sunt accesați prin calea completă, de exemplu App.Utils.log; App.log ar fi o eroare, pentru că log nu este membru direct al lui App.

Prompt
Arată namespace-uri imbricate, accesate cu notația cu punct. Declară un namespace App care conține un namespace exportat Utils (cu funcțiile log și error, care afișează [LOG]: msg și [ERROR]: msg) și un namespace exportat Models (o interfață User cu id, name, email și o clasă UserService a cărei metodă getUser(id) returnează John Doe / john@example.com). Apelează App.Utils.log("Aplicația pornește"), încarcă utilizatorul 1 prin App.Models.UserService, afișează Utilizator încărcat: <name> și lasă un apel App.log(...) comentat, cu o notă că ar fi o eroare.
Ieșire: program TypeScript și rezultatul capturat

Aliasuri pentru namespace-uri

Poți crea un alias pentru un namespace sau pentru unul dintre membrii lui cu import Alias = Some.Long.Path. Acesta nu este un import de modul — doar dă un nume scurt unuia lung. Components este un alias pentru namespace-ul cel mai interior, iar Button este un alias pentru o singură clasă.

Prompt
Arată aliasuri de namespace create cu import Alias = Some.Long.Path. Declară VeryLongNamespace.DeeplyNested.Components (trei namespace-uri imbricate) care exportă clasele Button și TextField, ale căror metode display() afișează Buton afișat și Câmp de text afișat. Creează și afișează un buton în trei feluri: prin calea completă, printr-un alias Components al namespace-ului cel mai interior și printr-un alias Button al clasei înseși.
Ieșire: program TypeScript și rezultatul capturat

Namespace-uri răspândite în mai multe fișiere

Aplicațiile mari își împart adesea codul în mai multe fișiere. Un namespace din TypeScript poate fi răspândit peste mai multe fișiere și combinat la compilare cu ajutorul comentariilor de referință.

Un comentariu de referință precum /// <reference path="validators.ts" /> îi spune lui TypeScript că un fișier depinde de altul, ca să știe în ce ordine să le combine. Cele patru fișiere de mai jos formează împreună un singur namespace Validation.

Pentru a compila aceste fișiere într-un singur fișier JavaScript, folosește tsc --outFile sample.js main.ts.

validators.ts

Primul fișier declară namespace-ul Validation și exportă interfața StringValidator.

script.ts
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
}

letters-validator.ts

Acest fișier face referire la validators.ts și extinde același namespace Validation cu o clasă LettersValidator.

script.ts
/// <reference path="validators.ts" />
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

zipcode-validator.ts

Și acest fișier face referire la validators.ts și adaugă o clasă ZipCodeValidator în namespace.

script.ts
/// <reference path="validators.ts" />
namespace Validation {
  const zipCodeRegexp = /^[0-9]+$/;

  export class ZipCodeValidator implements StringValidator {
    isValid(s: string): boolean {
      return zipCodeRegexp.test(s) && s.length === 5;
    }
  }
}

main.ts

Fișierul principal face referire la toate cele trei fișiere, creează ambii validatori și afișează dacă fiecare șir de test se potrivește cu fiecare validator.

script.ts
/// <reference path="validators.ts" />
/// <reference path="letters-validator.ts" />
/// <reference path="zipcode-validator.ts" />

// Now you can use the validators from multiple files
let validators: { [s: string]: Validation.StringValidator } = {};
validators["letters"] = new Validation.LettersValidator();
validators["zipcode"] = new Validation.ZipCodeValidator();

// Some samples to validate
let strings = ["Hello", "98052", "101"];

// Validate each
strings.forEach(s => {
  for (let name in validators) {
    console.log(`"${s}" - ${validators[name].isValid(s) ? "matches" : "does not match"} ${name}`);
  }
});

Namespace-uri față de module

Este important să știi când folosești namespace-uri și când folosești module ES:

  • Modulele sunt modalitatea preferată de organizare a codului în aplicațiile TypeScript moderne
  • Namespace-urile sunt încă utile în cazuri specifice, cum ar fi unirea declarațiilor sau lucrul cu cod vechi
  • Modulele au un suport mai bun în unelte și permit tree-shaking
  • Namespace-urile pot fi practice pentru crearea de biblioteci globale

Iată o comparație, funcționalitate cu funcționalitate, între namespace-uri și modulele ES (import/export):

  • Domeniu și scară recomandate — Namespace-uri: configurări mai simple, aplicații mici, proiecte vechi. Module: aplicații moderne de orice dimensiune; preferate pentru proiecte noi.
  • Sintaxă și utilizare — Namespace-uri: acces global prin notația cu punct (de exemplu MyNS.Member). Module: import/export explicite, cu căi de fișiere.
  • Încărcare și împachetare — Namespace-uri: nu au nevoie de un încărcător; pot genera un singur fișier cu --outFile. Module: trec de obicei printr-un bundler sau un încărcător (Vite, webpack etc.).
  • Împărțirea în mai multe fișiere — Namespace-uri: posibilă prin comentarii /// <reference />. Module: naturală; fiecare fișier este un modul cu exporturi și importuri explicite.
  • Tree-shaking — Namespace-uri: limitat; uneltelor de împachetare le este greu să elimine codul nefolosit. Module: excelent; sunt proiectate pentru eliminarea codului mort.
  • Domeniul global — Namespace-uri: încurajează variabilele globale (grupate într-un namespace). Module: evită variabilele globale; dependențele sunt explicite.
  • Extindere și unire — Namespace-uri: suport puternic prin unirea declarațiilor. Module: extinderea unui modul este posibilă, dar mai limitată.
  • Unelte și ecosistem — Namespace-uri: funcționează, dar se potrivesc mai puțin cu uneltele moderne. Module: cel mai bun suport în uneltele și platformele moderne.
  • Cel mai potrivite pentru — Namespace-uri: biblioteci vechi, scripturi globale, tipuri ambientale. Module: toată dezvoltarea nouă, biblioteci și aplicații.

Tipare avansate cu namespace-uri

Încă două tipare sunt frecvente în lucrul cu namespace-uri: extinderea unui namespace existent prin unirea declarațiilor și plasarea tipurilor și claselor generice într-un namespace.

Extinderea unui namespace

Un al doilea bloc declare namespace Express se unește cu primul, adăugând requestTime și log la Request, plus o nouă interfață UserSession; codul de utilizare presupune o funcție express() dintr-o bibliotecă.

script.ts
// Original namespace
declare namespace Express {
  interface Request {
    user?: { id: number; name: string };
  }
  interface Response {
    json(data: any): void;
  }
}

// Later in your application (e.g., in a .d.ts file)
declare namespace Express {
  // Augment the Request interface
  interface Request {
    // Add custom properties
    requestTime?: number;
    // Add methods
    log(message: string): void;
  }

  // Add new types
  interface UserSession {
    userId: number;
    expires: Date;
  }
}

// Usage in your application
const app = express();

app.use((req: Express.Request, res: Express.Response, next) => {
  // Augmented properties and methods are available
  req.requestTime = Date.now();
  req.log('Request started');
  next();
});

Namespace-uri cu generice

Namespace-ul DataStorage exportă o interfață generică Repository<T> și o clasă InMemoryRepository<T> care o implementează, folosită aici cu un tip User.

script.ts
// Generic namespace example
namespace DataStorage {
  export interface Repository<T> {
    getAll(): T[];
    getById(id: number): T | undefined;
    add(item: T): void;
    update(id: number, item: T): boolean;
    delete(id: number): boolean;
  }

  // Concrete implementation
  export class InMemoryRepository<T> implements Repository<T> {
    private items: T[] = [];

    getAll(): T[] {
      return [...this.items];
    }

    getById(id: number): T | undefined {
      return this.items[id];
    }

    add(item: T): void {
      this.items.push(item);
    }

    update(id: number, item: T): boolean {
      if (id >= 0 && id < this.items.length) {
        this.items[id] = item;
        return true;
      }
      return false;
    }

    delete(id: number): boolean {
      if (id >= 0 && id < this.items.length) {
        this.items.splice(id, 1);
        return true;
      }
      return false;
    }
  }
}

// Usage
interface User {
  id: number;
  name: string;
  email: string;
}

const userRepo = new DataStorage.InMemoryRepository<User>();
userRepo.add({ id: 1, name: 'John Doe', email: 'john@example.com' });
const allUsers = userRepo.getAll();

Bune practici

Ce să faci:

  • Folosește nume de namespace clare și ierarhice
  • Exportă dintr-un namespace doar ce este necesar
  • Folosește comentarii /// <reference /> pentru a ordona fișierele în namespace-urile răspândite în mai multe fișiere
  • Ia în calcul modulele pentru proiectele noi
  • Folosește const enum în interiorul namespace-urilor pentru performanță mai bună
  • Documentează-ți namespace-urile cu comentarii JSDoc

Ce să nu faci:

  • Să creezi ierarhii de namespace foarte adânci (mai mult de 2–3 niveluri)
  • Să poluezi inutil domeniul global
  • Să amesteci namespace-uri și module în același proiect fără o strategie clară
  • Să folosești namespace-uri pentru aplicații mici — preferă modulele

Aspecte legate de performanță:

  • Namespace-urile mari pot crește dimensiunea pachetului final
  • Ia în calcul împărțirea codului pentru aplicațiile mari
  • Ai grijă la dependențele circulare în structuri complexe de namespace
  • Folosește const enum pentru performanță mai bună cu valori constante

Migrarea de la namespace-uri la module

Trecerea de la namespace-uri la module ES înseamnă de obicei transformarea fiecărui namespace în propriul fișier, cu membri marcați export, și înlocuirea accesului cu punct prin instrucțiuni import.

Pașii migrării:

  1. Transformă fiecare namespace într-un fișier de modul
  2. Înlocuiește export-urile din namespace cu exporturi de modul ES
  3. Actualizează importurile ca să folosească sintaxa de modul ES
  4. Configurează sistemul de build ca să lucreze cu module
  5. Actualizează testele ca să funcționeze cu noua structură de module
  6. Ia în calcul folosirea unui bundler precum webpack sau Rollup
  7. Actualizează tsconfig.json ca să folosească "module": "ESNext"

Unelte pentru migrare:

  • ts-migrate — o unealtă automată de migrare de la Facebook
  • tslint cu regula no-namespace, pentru a detecta namespace-urile
  • Uneltele de refactorizare incluse în TypeScript

Înainte și după migrare

Aceeași clasă UserService stă întâi în namespace-uri imbricate, apoi devine o clasă exportată în propriul fișier de modul, care este importat acolo unde e nevoie.

script.ts
// Before: Using namespaces
namespace MyApp {
  export namespace Services {
    export class UserService {
      getUser(id: number) { /* ... */ }
    }
  }
}

// After: Using ES modules
// services/UserService.ts
export class UserService {
  getUser(id: number) { /* ... */ }
}

// app.ts
import { UserService } from './services/UserService';
const userService = new UserService();