Classes em TypeScript: visibilidade e modificadores

1. Introdução à Visibilidade em Classes TypeScript

Modificadores de acesso são palavras-chave que controlam onde propriedades e métodos de uma classe podem ser acessados. Em JavaScript, a privacidade sempre foi uma convenção — desenvolvedores usavam _ como prefixo para indicar membros "privados", mas nada impedia o acesso externo. TypeScript introduz controle real em tempo de compilação, garantindo que violações de encapsulamento sejam detectadas antes da execução.

TypeScript oferece três modificadores de visibilidade: public (acessível em qualquer lugar), protected (acessível na classe e subclasses) e private (acessível apenas na própria classe). Vamos explorar cada um em detalhes.

2. O Modificador public – Comportamento Padrão

public é o modificador implícito em TypeScript. Se nenhum modificador for especificado, o membro é considerado público. Isso significa que pode ser acessado de dentro da classe, de subclasses e de qualquer código externo.

class LoggerService {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }

  formatMessage(text: string): string {
    return `[${new Date().toISOString()}] ${text}`;
  }
}

const logger = new LoggerService();
logger.log("Serviço iniciado"); // Acesso externo permitido
logger.formatMessage("teste");  // Também permitido

Embora útil, expor tudo como público quebra o encapsulamento. A boa prática é tornar público apenas o que faz parte da API da classe.

3. O Modificador private – Encapsulamento Restrito

Membros private só podem ser acessados dentro da própria classe que os declara. Nem mesmo subclasses têm acesso.

class Account {
  private balance: number;
  private transactionHistory: string[] = [];

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

  private addTransaction(type: string, amount: number): void {
    this.transactionHistory.push(`${type}: R$${amount.toFixed(2)}`);
  }

  deposit(amount: number): void {
    this.balance += amount;
    this.addTransaction("Depósito", amount);
  }

  getBalance(): number {
    return this.balance;
  }
}

const account = new Account(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// account.balance;        // Erro: 'balance' é privado
// account.addTransaction("Teste", 100); // Erro: método privado

Diferença entre private do TypeScript e # do JavaScript: O modificador private do TypeScript é uma verificação apenas em tempo de compilação. O JavaScript gerado não mantém essa restrição. Já o campo # (privado nativo do JavaScript) oferece encapsulamento real em tempo de execução, mas não é intercambiável com private do TypeScript.

class ModernAccount {
  #balance: number; // Campo privado nativo (ES2020+)

  constructor(initialBalance: number) {
    this.#balance = initialBalance;
  }

  getBalance(): number {
    return this.#balance;
  }
}

4. O Modificador protected – Visibilidade em Hierarquias

Membros protected são acessíveis na classe base e em qualquer subclasse, mas não fora da cadeia de herança.

abstract class Shape {
  protected calculateAreaBase(): number {
    return 0; // Implementação base
  }

  abstract getArea(): number;
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }

  logBaseCalculation(): void {
    console.log(`Área base: ${this.calculateAreaBase()}`); // Acesso permitido
  }
}

class Square extends Shape {
  constructor(private side: number) {
    super();
  }

  getArea(): number {
    return this.side * this.side;
  }
}

const circle = new Circle(5);
circle.getArea(); // OK
// circle.calculateAreaBase(); // Erro: 'calculateAreaBase' é protegido

5. Modificadores de Parâmetros no Construtor

TypeScript oferece uma sintaxe reduzida para declarar e inicializar propriedades diretamente no construtor, combinando declaração, tipo e modificador de visibilidade.

class User {
  constructor(
    public name: string,
    private readonly id: string,
    protected email: string
  ) {
    // As propriedades já estão declaradas e inicializadas
  }

  displayInfo(): void {
    console.log(`Usuário: ${this.name}, ID: ${this.id}`);
  }
}

const user = new User("Alice", "uuid-123", "alice@email.com");
console.log(user.name);      // OK
// console.log(user.id);     // Erro: 'id' é privado
// console.log(user.email);  // Erro: 'email' é protegido

Essa sintaxe elimina a repetição de declarar a propriedade e depois atribuí-la no corpo do construtor.

6. Modificador readonly – Imutabilidade de Propriedades

O modificador readonly impede que uma propriedade seja reatribuída após a inicialização. Pode ser combinado com qualquer modificador de visibilidade.

class AppConfig {
  constructor(
    public readonly appName: string,
    private readonly apiKey: string,
    protected readonly version: string = "1.0.0"
  ) {}

  getApiKey(): string {
    return this.apiKey;
  }
}

const config = new AppConfig("MyApp", "sk-12345");
console.log(config.appName); // "MyApp"
// config.appName = "NewName"; // Erro: 'appName' é readonly
console.log(config.getApiKey()); // "sk-12345"

Propriedades readonly podem ser atribuídas apenas na declaração ou dentro do construtor. Qualquer tentativa de modificação posterior gera erro de compilação.

7. Modificador static – Membros de Classe vs. Membros de Instância

Membros static pertencem à classe, não a instâncias individuais. São acessados através do nome da classe e podem combinar com qualquer modificador de visibilidade.

class InstanceCounter {
  private static instanceCount = 0;
  public static readonly MAX_INSTANCES = 100;

  constructor() {
    if (InstanceCounter.instanceCount >= InstanceCounter.MAX_INSTANCES) {
      throw new Error("Limite máximo de instâncias atingido");
    }
    InstanceCounter.incrementCount();
  }

  private static incrementCount(): void {
    InstanceCounter.instanceCount++;
  }

  static getCurrentCount(): number {
    return InstanceCounter.instanceCount;
  }
}

const obj1 = new InstanceCounter();
const obj2 = new InstanceCounter();
console.log(InstanceCounter.getCurrentCount()); // 2
console.log(InstanceCounter.MAX_INSTANCES);      // 100
// InstanceCounter.incrementCount(); // Erro: método privado

Membros estáticos privados são úteis para contadores, caches ou configurações globais que não devem ser expostos.

8. Considerações Finais e Boas Práticas

Quando usar cada modificador:

  • public: Para a API pública da classe — métodos e propriedades que outros módulos precisam consumir.
  • private: Para detalhes internos de implementação — dados sensíveis, lógica auxiliar, estado que não deve ser exposto.
  • protected: Quando subclasses precisam de acesso, mas o mundo externo não. Ideal para frameworks e bibliotecas que esperam extensão.

Encapsulamento na herança: private impede acesso mesmo em subclasses; protected permite. Escolha protected apenas se a hierarquia realmente precisar desse acesso.

Limitação importante: TypeScript não emite verificações de visibilidade no JavaScript gerado. Se o código for transpilado para ES5, membros private e protected serão simples propriedades acessíveis em tempo de execução. Para proteção real em runtime, use campos privados nativos (#).

Combinações poderosas: private readonly para valores imutáveis internos, protected static para utilitários compartilhados em hierarquias, public static readonly para constantes da classe.

class BestPractices {
  // API pública
  public execute(): void {
    this.validate();
    this.process();
  }

  // Detalhes internos
  private validate(): boolean {
    return true;
  }

  // Acessível por subclasses
  protected process(): void {
    console.log("Processando...");
  }

  // Constante pública
  public static readonly VERSION = "2.0.0";

  // Cache interno
  private static cache = new Map<string, unknown>();
}

Dominar os modificadores de visibilidade e acesso em TypeScript permite criar APIs mais limpas, seguras e expressivas, aproveitando ao máximo o sistema de tipos da linguagem.

Referências