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.

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 modificadorprivatede 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
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.