Clases y tipado en TypeScript

Intermedio
TypeScript
TypeScript
Actualizado: 18/04/2026

Propiedades tipadas y modificadores de acceso

TypeScript permite declarar propiedades en las clases con anotaciones de tipo explícitas, algo que JavaScript puro no ofrece. Esto garantiza que cada propiedad almacene exclusivamente valores del tipo esperado, detectando errores en tiempo de compilación.

Anatomia de clases tipadas en TypeScript

class Producto {
  nombre: string
  precio: number
  disponible: boolean

  constructor(nombre: string, precio: number, disponible: boolean) {
    this.nombre = nombre
    this.precio = precio
    this.disponible = disponible
  }
}

const laptop = new Producto("Laptop Pro", 1299.99, true)
console.log(laptop.nombre) // "Laptop Pro"

Cada propiedad queda vinculada a un tipo concreto. Si intentamos asignar un valor incompatible, TypeScript genera un error de compilación antes de que el código se ejecute.

Modificador public

El modificador public es el valor por defecto en TypeScript. Todas las propiedades y métodos son públicos a menos que se indique lo contrario. Escribirlo de forma explícita es opcional pero puede mejorar la legibilidad:

class Usuario {
  public nombre: string
  public email: string

  constructor(nombre: string, email: string) {
    this.nombre = nombre
    this.email = email
  }

  public mostrarInfo(): string {
    return `${this.nombre} <${this.email}>`
  }
}

const usuario = new Usuario("Ana", "ana@ejemplo.com")
console.log(usuario.nombre) // Acceso directo permitido
console.log(usuario.mostrarInfo())

Modificador private

El modificador private restringe el acceso a la propiedad o método exclusivamente al interior de la clase donde se declara. Ni las subclases ni el código externo pueden acceder:

class CuentaBancaria {
  private saldo: number
  private titular: string

  constructor(titular: string, saldoInicial: number) {
    this.titular = titular
    this.saldo = saldoInicial
  }

  depositar(cantidad: number): void {
    if (cantidad <= 0) {
      throw new Error("La cantidad debe ser positiva")
    }
    this.saldo += cantidad
  }

  retirar(cantidad: number): void {
    if (cantidad > this.saldo) {
      throw new Error("Saldo insuficiente")
    }
    this.saldo -= cantidad
  }

  obtenerSaldo(): number {
    return this.saldo
  }
}

const cuenta = new CuentaBancaria("Carlos", 1000)
cuenta.depositar(500)
console.log(cuenta.obtenerSaldo()) // 1500
// cuenta.saldo // Error: Property 'saldo' is private

El modificador private de TypeScript es una restricción en tiempo de compilación. El código JavaScript generado no impide el acceso en tiempo de ejecución. Para privacidad real en tiempo de ejecución se utilizan los campos privados con #.

Modificador protected

El modificador protected permite el acceso desde la propia clase y desde cualquier subclase que la extienda, pero no desde código externo:

class Vehiculo {
  protected velocidadMaxima: number
  protected velocidadActual: number

  constructor(velocidadMaxima: number) {
    this.velocidadMaxima = velocidadMaxima
    this.velocidadActual = 0
  }

  protected validarVelocidad(velocidad: number): boolean {
    return velocidad >= 0 && velocidad <= this.velocidadMaxima
  }
}

class Coche extends Vehiculo {
  private marchas: number

  constructor(velocidadMaxima: number, marchas: number) {
    super(velocidadMaxima)
    this.marchas = marchas
  }

  acelerar(incremento: number): void {
    const nuevaVelocidad = this.velocidadActual + incremento
    if (this.validarVelocidad(nuevaVelocidad)) {
      this.velocidadActual = nuevaVelocidad
    }
  }

  obtenerEstado(): string {
    return `Velocidad: ${this.velocidadActual}/${this.velocidadMaxima} km/h`
  }
}

const coche = new Coche(220, 6)
coche.acelerar(80)
console.log(coche.obtenerEstado()) // "Velocidad: 80/220 km/h"
// coche.velocidadMaxima // Error: Property 'velocidadMaxima' is protected

Modificador readonly

El modificador readonly impide la reasignación de una propiedad después de su inicialización. Solo puede asignarse en la declaración o en el constructor:

class Configuración {
  readonly versión: string
  readonly maxConexiones: number
  readonly createdAt: Date

  constructor(versión: string, maxConexiones: number) {
    this.versión = versión
    this.maxConexiones = maxConexiones
    this.createdAt = new Date()
  }
}

const config = new Configuración("2.0.1", 100)
console.log(config.versión) // "2.0.1"
// config.versión = "3.0.0" // Error: Cannot assign to 'versión' because it is a read-only property

Se puede combinar readonly con otros modificadores de acceso:

class Servidor {
  private readonly id: string
  protected readonly host: string
  public readonly puerto: number

  constructor(id: string, host: string, puerto: number) {
    this.id = id
    this.host = host
    this.puerto = puerto
  }

  obtenerUrl(): string {
    return `${this.host}:${this.puerto}`
  }
}

Campos privados con # y parameter properties

Campos privados con

JavaScript moderno ofrece campos privados nativos usando el prefijo #. A diferencia del modificador private de TypeScript, los campos # proporcionan privacidad real en tiempo de ejecución:

class Token {
  #valor: string
  #expiracion: Date

  constructor(valor: string, duracionMs: number) {
    this.#valor = valor
    this.#expiracion = new Date(Date.now() + duracionMs)
  }

  esValido(): boolean {
    return new Date() < this.#expiracion
  }

  obtenerValor(): string {
    if (!this.esValido()) {
      throw new Error("Token expirado")
    }
    return this.#valor
  }
}

const token = new Token("abc123secret", 60000)
console.log(token.esValido()) // true
console.log(token.obtenerValor()) // "abc123secret"
// token.#valor // Error en compilación y en tiempo de ejecución

Los campos con # son verdaderamente inaccesibles desde fuera de la clase, incluso en el JavaScript compilado. El modificador private de TypeScript solo genera un error de compilación pero no protege el acceso en runtime.

Las diferencias principales entre ambos enfoques:

class Comparativa {
  private softPrivate: string = "accesible con bracket notation"
  #hardPrivate: string = "inaccesible desde fuera"

  demostrar(): void {
    console.log(this.softPrivate)
    console.log(this.#hardPrivate)
  }
}

const obj = new Comparativa()
// (obj as any).softPrivate // Funciona en runtime
// (obj as any)["softPrivate"] // Funciona en runtime
// obj.#hardPrivate // Error real en runtime

Parameter properties

TypeScript ofrece una sintaxis abreviada llamada parameter properties que permite declarar y asignar propiedades directamente en los parámetros del constructor. Se activa prefijando el parámetro con un modificador de visibilidad (public, private, protected) o con readonly:

class Empleado {
  constructor(
    public nombre: string,
    private salario: number,
    protected departamento: string,
    public readonly id: number
  ) {}

  mostrarInfo(): string {
    return `${this.nombre} (${this.departamento}) - ID: ${this.id}`
  }

  obtenerSalario(): number {
    return this.salario
  }
}

const empleado = new Empleado("Laura", 45000, "Ingenieria", 1001)
console.log(empleado.nombre) // "Laura"
console.log(empleado.id) // 1001
console.log(empleado.mostrarInfo())

El código anterior es equivalente a declarar las propiedades por separado y asignarlas manualmente en el constructor, pero con mucho menos código. Esto resulta especialmente útil en clases con muchas propiedades:

class Pedido {
  constructor(
    public readonly id: string,
    private readonly items: string[],
    public readonly fecha: Date,
    private estado: string = "pendiente"
  ) {}

  confirmar(): void {
    this.estado = "confirmado"
  }

  obtenerResumen(): string {
    return `Pedido ${this.id}: ${this.items.length} items - ${this.estado}`
  }
}

const pedido = new Pedido("PED-001", ["Laptop", "Raton"], new Date())
pedido.confirmar()
console.log(pedido.obtenerResumen())

Se pueden combinar parameter properties con parámetros normales sin modificador:

class Logger {
  private registros: string[] = []

  constructor(
    public readonly nombre: string,
    private nivelMinimo: number,
    prefijo?: string
  ) {
    if (prefijo) {
      this.registros.push(`[${prefijo}] Logger inicializado`)
    }
  }

  log(mensaje: string, nivel: number): void {
    if (nivel >= this.nivelMinimo) {
      this.registros.push(`[${this.nombre}] ${mensaje}`)
    }
  }

  obtenerRegistros(): string[] {
    return [...this.registros]
  }
}

Miembros estáticos tipados y strictPropertyInitialization

Miembros estáticos tipados

Los miembros static pertenecen a la clase en sí, no a las instancias individuales. TypeScript permite tiparlos y aplicarles los mismos modificadores de acceso:

class Contador {
  private static instancias: number = 0
  public static readonly VERSION: string = "1.0.0"

  readonly id: number

  constructor(public nombre: string) {
    Contador.instancias++
    this.id = Contador.instancias
  }

  static obtenerTotalInstancias(): number {
    return Contador.instancias
  }

  static resetear(): void {
    Contador.instancias = 0
  }
}

const c1 = new Contador("Primero")
const c2 = new Contador("Segundo")
console.log(Contador.obtenerTotalInstancias()) // 2
console.log(Contador.VERSION) // "1.0.0"

Un patrón habitual es el singleton con miembros estáticos privados:

class BaseDatos {
  private static instancia: BaseDatos | null = null
  private conexiones: Map<string, string> = new Map()

  private constructor(private readonly host: string) {}

  static obtenerInstancia(host: string = "localhost"): BaseDatos {
    if (!BaseDatos.instancia) {
      BaseDatos.instancia = new BaseDatos(host)
    }
    return BaseDatos.instancia
  }

  conectar(nombre: string): void {
    this.conexiones.set(nombre, `Conectado a ${this.host}`)
  }

  obtenerConexiones(): Map<string, string> {
    return new Map(this.conexiones)
  }
}

const db1 = BaseDatos.obtenerInstancia("db.ejemplo.com")
const db2 = BaseDatos.obtenerInstancia()
console.log(db1 === db2) // true

Los métodos estáticos también pueden tener tipos genéricos y devolver tipos específicos:

class Coleccion<T> {
  private items: T[] = []

  static crear<U>(items: U[]): Coleccion<U> {
    const colección = new Coleccion<U>()
    for (const item of items) {
      colección.agregar(item)
    }
    return colección
  }

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

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

  obtenerPrimero(): T | undefined {
    return this.items[0]
  }
}

const números = Coleccion.crear([1, 2, 3])
console.log(números.obtenerTodos()) // [1, 2, 3]

const textos = Coleccion.crear(["hola", "mundo"])
console.log(textos.obtenerPrimero()) // "hola"

strictPropertyInitialization

La opción strictPropertyInitialization del compilador TypeScript exige que toda propiedad de clase se inicialice en la declaración o en el constructor. Sin ella, es posible olvidar la inicialización y encontrar errores en tiempo de ejecución:

// Con strictPropertyInitialization: true
class Formulario {
  titulo: string // Error: Property 'titulo' has no initializer
  campos: string[] // Error: Property 'campos' has no initializer

  constructor() {
    // Olvidamos inicializar las propiedades
  }
}

Para cumplir con está regla se pueden usar varias estrategias:

// Estrategia 1: Inicializar en la declaración
class Config {
  modo: string = "producción"
  intentos: number = 3
  activo: boolean = true
}

// Estrategia 2: Inicializar en el constructor
class Sesion {
  usuario: string
  token: string
  inicio: Date

  constructor(usuario: string, token: string) {
    this.usuario = usuario
    this.token = token
    this.inicio = new Date()
  }
}

// Estrategia 3: Usar parameter properties
class Conexion {
  constructor(
    public host: string,
    public puerto: number,
    private readonly protocolo: string = "https"
  ) {}
}

Cuando una propiedad se inicializa fuera del constructor (por ejemplo, mediante un método de configuración externo), se puede usar el operador de aserción de asignación definida !:

class ComponenteUI {
  elemento!: HTMLElement // Le indicamos a TypeScript que sera inicializado antes de usarse
  private datos!: string[]

  inicializar(selector: string): void {
    const el = document.querySelector(selector)
    if (el instanceof HTMLElement) {
      this.elemento = el
    }
    this.datos = []
  }

  renderizar(): void {
    this.elemento.textContent = this.datos.join(", ")
  }
}

El operador ! en la declaración de propiedades indica a TypeScript que la propiedad será inicializada antes de su uso. Debe usarse con precaución, ya que desactiva la comprobación de inicialización para esa propiedad específica.

Un ejemplo completo que combina todas las funcionalidades de tipado en clases:

class GestorTareas {
  private static contadorId: number = 0
  public static readonly PRIORIDAD_MAXIMA: number = 5

  #tareas: Map<number, { titulo: string; completada: boolean; prioridad: number }>

  constructor(
    public readonly nombre: string,
    private readonly propietario: string
  ) {
    this.#tareas = new Map()
  }

  agregarTarea(titulo: string, prioridad: number = 1): number {
    if (prioridad < 1 || prioridad > GestorTareas.PRIORIDAD_MAXIMA) {
      throw new Error(`Prioridad debe estar entre 1 y ${GestorTareas.PRIORIDAD_MAXIMA}`)
    }
    const id = ++GestorTareas.contadorId
    this.#tareas.set(id, { titulo, completada: false, prioridad })
    return id
  }

  completarTarea(id: number): void {
    const tarea = this.#tareas.get(id)
    if (!tarea) {
      throw new Error(`Tarea ${id} no encontrada`)
    }
    tarea.completada = true
  }

  obtenerPendientes(): string[] {
    const pendientes: string[] = []
    for (const [, tarea] of this.#tareas) {
      if (!tarea.completada) {
        pendientes.push(tarea.titulo)
      }
    }
    return pendientes
  }

  obtenerEstadisticas(): { total: number; completadas: number; pendientes: number } {
    let completadas = 0
    for (const [, tarea] of this.#tareas) {
      if (tarea.completada) completadas++
    }
    return {
      total: this.#tareas.size,
      completadas,
      pendientes: this.#tareas.size - completadas
    }
  }
}

const gestor = new GestorTareas("Sprint 1", "equipo-dev")
const id1 = gestor.agregarTarea("Implementar login", 3)
const id2 = gestor.agregarTarea("Escribir tests", 2)
gestor.completarTarea(id1)
console.log(gestor.obtenerPendientes()) // ["Escribir tests"]
console.log(gestor.obtenerEstadisticas()) // { total: 2, completadas: 1, pendientes: 1 }
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender las propiedades tipadas en clases TypeScript. Dominar los modificadores de acceso public, private, protected y readonly. Diferenciar entre private de TypeScript y campos privados con #. Aplicar parameter properties para simplificar constructores. Usar miembros estaticos tipados y strictPropertyInitialization.