TypeScript

TypeScript

Tutorial TypeScript: Clases y objetos

TypeScript clases objetos: creación y uso. Domina la creación y uso de clases y objetos en TypeScript con ejemplos prácticos y detallados.

Aprende TypeScript y certifícate

Definición de clases en TypeScript

TypeScript, como lenguaje que extiende JavaScript, incorpora un sistema de clases robusto que permite implementar programación orientada a objetos de manera más estructurada y tipada. Las clases en TypeScript representan plantillas para crear objetos, definiendo su estructura, comportamiento y tipo.

Una clase en TypeScript actúa como un plano que describe qué propiedades y métodos tendrán los objetos creados a partir de ella. Esta característica es fundamental para organizar código en aplicaciones complejas y facilitar la reutilización.

Sintaxis básica

La definición de una clase en TypeScript utiliza la palabra clave class seguida del nombre de la clase (por convención en PascalCase) y un bloque de código delimitado por llaves:

class Person {
  // Contenido de la clase
}

Propiedades de clase

Las propiedades son las variables que almacenan datos dentro de una clase. En TypeScript, es recomendable declarar las propiedades antes de usarlas, especificando su tipo:

class Person {
  name: string;
  age: number;
}

TypeScript permite declarar propiedades con diferentes modificadores:

class Product {
  // Propiedad pública (accesible desde cualquier lugar)
  id: number;
  
  // Propiedad con valor inicial
  name: string = "Default Product";
  
  // Propiedad de solo lectura (no puede modificarse después de inicializarse)
  readonly code: string;
}

Inicialización de propiedades

Existen varias formas de inicializar las propiedades de una clase:

  • Inicialización en la declaración:
class Counter {
  value: number = 0;
  readonly maxValue: number = 100;
}
  • Inicialización en el constructor (lo veremos con más detalle en la sección de constructores):
class User {
  username: string;
  
  constructor(username: string) {
    this.username = username;
  }
}

Métodos de clase

Los métodos son funciones definidas dentro de una clase que describen el comportamiento de los objetos:

class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
  
  subtract(a: number, b: number): number {
    return a - b;
  }
}

Los métodos pueden acceder a las propiedades de la clase usando la palabra clave this:

class Counter {
  value: number = 0;
  
  increment(): void {
    this.value += 1;
  }
  
  reset(): void {
    this.value = 0;
  }
  
  getCurrentValue(): number {
    return this.value;
  }
}

Propiedades y métodos estáticos

Los miembros estáticos pertenecen a la clase en sí misma, no a las instancias individuales:

class MathUtils {
  static PI: number = 3.14159;
  
  static calculateCircleArea(radius: number): number {
    return MathUtils.PI * radius * radius;
  }
}

// Uso sin instanciar la clase
const area = MathUtils.calculateCircleArea(5);
console.log(MathUtils.PI); // 3.14159

Sintaxis abreviada para propiedades

TypeScript ofrece una sintaxis abreviada para declarar e inicializar propiedades directamente en el constructor:

class Person {
  constructor(
    public name: string,
    public age: number,
    private id: string
  ) {
    // No es necesario asignar this.name = name, etc.
    // TypeScript lo hace automáticamente
  }
}

Esta sintaxis es equivalente a:

class Person {
  public name: string;
  public age: number;
  private id: string;
  
  constructor(name: string, age: number, id: string) {
    this.name = name;
    this.age = age;
    this.id = id;
  }
}

Propiedades opcionales

También podemos definir propiedades opcionales en una clase usando el operador ?:

class Article {
  title: string;
  content: string;
  tags?: string[]; // Propiedad opcional
  
  constructor(title: string, content: string) {
    this.title = title;
    this.content = content;
  }
}

Ejemplo práctico: Clase para gestionar tareas

Veamos un ejemplo más completo de una clase para gestionar tareas:

class Task {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
  
  constructor(id: number, title: string, description: string = "") {
    this.id = id;
    this.title = title;
    this.description = description;
    this.completed = false;
    this.createdAt = new Date();
  }
  
  markAsCompleted(): void {
    this.completed = true;
  }
  
  updateTitle(newTitle: string): void {
    this.title = newTitle;
  }
  
  getTaskInfo(): string {
    return `Task ${this.id}: ${this.title} (${this.completed ? 'Completed' : 'Pending'})`;
  }
}

Esta clase Task define la estructura para objetos de tipo tarea, con propiedades como id, title, description, etc., y métodos para manipular el estado de la tarea.

Clases y tipos

Una característica importante de TypeScript es que las clases funcionan simultáneamente como tipos:

class Point {
  x: number;
  y: number;
  
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

// La clase Point actúa como un tipo
function calculateDistance(p1: Point, p2: Point): number {
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  return Math.sqrt(dx*dx + dy*dy);
}

const point1 = new Point(0, 0);
const point2 = new Point(3, 4);
console.log(calculateDistance(point1, point2)); // 5

En este ejemplo, la clase Point se utiliza como tipo para los parámetros de la función calculateDistance.

Las clases en TypeScript proporcionan una base sólida para implementar programación orientada a objetos, permitiendo organizar el código de manera estructurada y tipada. En las siguientes secciones, exploraremos cómo crear instancias de estas clases y profundizaremos en conceptos como constructores y herencia.

Creación e instanciación de objetos

Una vez definida una clase en TypeScript, el siguiente paso es crear objetos a partir de ella. Este proceso, conocido como instanciación, nos permite utilizar el plano definido por la clase para generar elementos concretos con los que trabajar en nuestra aplicación.

La instanciación de objetos en TypeScript sigue un patrón similar al de otros lenguajes orientados a objetos, pero con las ventajas adicionales del sistema de tipos estático.

Sintaxis básica de instanciación

Para crear un objeto a partir de una clase, utilizamos el operador new seguido del nombre de la clase y paréntesis (que invocan al constructor):

class User {
  username: string;
  isActive: boolean = false;
  
  constructor(username: string) {
    this.username = username;
  }
}

// Creación de un objeto User
const user1 = new User("alice123");

En este ejemplo, user1 es una instancia de la clase User y tiene acceso a todas las propiedades y métodos definidos en esa clase.

Acceso a propiedades y métodos

Una vez creado un objeto, podemos acceder a sus propiedades y métodos utilizando la notación de punto:

class Product {
  name: string;
  price: number;
  
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
  
  applyDiscount(percentage: number): number {
    return this.price * (1 - percentage / 100);
  }
}

// Instanciación
const laptop = new Product("Gaming Laptop", 1200);

// Acceso a propiedades
console.log(laptop.name);  // "Gaming Laptop"
console.log(laptop.price); // 1200

// Invocación de métodos
const discountedPrice = laptop.applyDiscount(15);
console.log(discountedPrice); // 1020

Tipado de instancias

Una característica poderosa de TypeScript es que las instancias están tipadas según su clase de origen:

class Car {
  brand: string;
  model: string;
  
  constructor(brand: string, model: string) {
    this.brand = brand;
    this.model = model;
  }
  
  getFullName(): string {
    return `${this.brand} ${this.model}`;
  }
}

// La variable tesla es de tipo Car
const tesla = new Car("Tesla", "Model 3");

// TypeScript conoce la estructura de tesla
console.log(tesla.brand);     // OK
console.log(tesla.getFullName()); // OK
console.log(tesla.color);     // Error: Property 'color' does not exist on type 'Car'

El sistema de tipos de TypeScript nos ayuda a evitar errores al intentar acceder a propiedades o métodos que no existen en la clase.

Múltiples instancias

Podemos crear múltiples instancias independientes a partir de la misma clase:

class Counter {
  count: number = 0;
  
  increment(): void {
    this.count++;
  }
  
  reset(): void {
    this.count = 0;
  }
}

// Creamos dos contadores independientes
const counterA = new Counter();
const counterB = new Counter();

counterA.increment();
counterA.increment();
counterB.increment();

console.log(counterA.count); // 2
console.log(counterB.count); // 1

counterA.reset();
console.log(counterA.count); // 0
console.log(counterB.count); // 1 (no se ve afectado)

Cada instancia mantiene su propio estado independiente, lo que permite tener múltiples objetos con diferentes valores pero el mismo comportamiento.

Instanciación con parámetros opcionales

Cuando una clase tiene parámetros opcionales en su constructor, podemos omitirlos al crear instancias:

class Message {
  content: string;
  sender: string;
  timestamp: Date;
  
  constructor(content: string, sender: string, timestamp?: Date) {
    this.content = content;
    this.sender = sender;
    this.timestamp = timestamp || new Date(); // Valor por defecto si no se proporciona
  }
}

// Con todos los parámetros
const message1 = new Message("Hello", "Alice", new Date(2023, 0, 15));

// Omitiendo el parámetro opcional
const message2 = new Message("Hi there", "Bob");

console.log(message1.timestamp); // Fecha específica proporcionada
console.log(message2.timestamp); // Fecha actual (valor por defecto)

Asignación de tipos con instancias

Podemos usar clases como tipos para variables y parámetros:

class Point {
  x: number;
  y: number;
  
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

// Declaración de variable con tipo Point
let position: Point;

// Asignación válida (instancia de Point)
position = new Point(10, 20);

// Error: el objeto literal no es una instancia de Point
// position = { x: 10, y: 20 };

// Función que acepta un parámetro de tipo Point
function movePoint(point: Point, dx: number, dy: number): Point {
  return new Point(point.x + dx, point.y + dy);
}

const newPosition = movePoint(position, 5, -3);

En este ejemplo, aunque un objeto literal con propiedades x e y tendría la misma estructura que una instancia de Point, TypeScript distingue entre ambos por su origen.

Ejemplo práctico: Sistema de gestión de biblioteca

Veamos un ejemplo más completo de creación y uso de instancias:

class Book {
  title: string;
  author: string;
  isbn: string;
  isAvailable: boolean = true;
  
  constructor(title: string, author: string, isbn: string) {
    this.title = title;
    this.author = author;
    this.isbn = isbn;
  }
  
  checkout(): boolean {
    if (this.isAvailable) {
      this.isAvailable = false;
      return true;
    }
    return false;
  }
  
  return(): void {
    this.isAvailable = true;
  }
}

class Library {
  name: string;
  books: Book[] = [];
  
  constructor(name: string) {
    this.name = name;
  }
  
  addBook(book: Book): void {
    this.books.push(book);
  }
  
  findBookByISBN(isbn: string): Book | undefined {
    return this.books.find(book => book.isbn === isbn);
  }
}

// Creación de instancias
const centralLibrary = new Library("Central City Library");

const book1 = new Book("TypeScript Programming", "John Doe", "TS-12345");
const book2 = new Book("JavaScript Patterns", "Jane Smith", "JS-67890");

// Uso de métodos para manipular objetos
centralLibrary.addBook(book1);
centralLibrary.addBook(book2);

const foundBook = centralLibrary.findBookByISBN("TS-12345");
if (foundBook) {
  const checkoutSuccess = foundBook.checkout();
  console.log(`Checkout successful: ${checkoutSuccess}`);
  console.log(`Book available: ${foundBook.isAvailable}`); // false
}

En este ejemplo, creamos instancias de las clases Book y Library, y demostramos cómo interactúan entre sí. Cada libro tiene su propio estado de disponibilidad, y la biblioteca mantiene una colección de libros.

Instanciación con patrones de diseño

La instanciación de objetos puede seguir patrones de diseño específicos para casos de uso particulares:

class ConfigManager {
  private static instance: ConfigManager | null = null;
  private settings: Map<string, any> = new Map();
  
  // Constructor privado (patrón Singleton)
  private constructor() {}
  
  // Método para obtener la única instancia
  static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }
  
  setSetting(key: string, value: any): void {
    this.settings.set(key, value);
  }
  
  getSetting(key: string): any {
    return this.settings.get(key);
  }
}

// Uso del patrón Singleton
const config1 = ConfigManager.getInstance();
config1.setSetting("theme", "dark");

const config2 = ConfigManager.getInstance();
console.log(config2.getSetting("theme")); // "dark" (misma instancia)

Este ejemplo implementa el patrón Singleton, que garantiza que solo exista una instancia de la clase ConfigManager en toda la aplicación.

La creación e instanciación de objetos es un paso fundamental para aprovechar el poder de las clases en TypeScript. Mediante la instanciación, transformamos las definiciones abstractas en objetos concretos con los que podemos trabajar, manteniendo todas las ventajas del sistema de tipos estático.

Constructores y parámetros

Los constructores son métodos especiales dentro de una clase que se ejecutan automáticamente cuando se crea una nueva instancia. En TypeScript, los constructores juegan un papel fundamental en la inicialización de objetos, permitiendo configurar el estado inicial de las propiedades y ejecutar cualquier lógica necesaria durante la creación.

El constructor se define mediante la palabra clave constructor y se invoca automáticamente al usar el operador new:

class Employee {
  name: string;
  position: string;
  
  constructor(name: string, position: string) {
    this.name = name;
    this.position = position;
    console.log(`New employee created: ${name}, ${position}`);
  }
}

const employee = new Employee("Sarah Johnson", "Software Engineer");

Constructores básicos

Un constructor simple recibe parámetros y los asigna a las propiedades de la clase:

class Rectangle {
  width: number;
  height: number;
  
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
  
  getArea(): number {
    return this.width * this.height;
  }
}

const rectangle = new Rectangle(10, 5);
console.log(rectangle.getArea()); // 50

Parámetros con valores predeterminados

Los constructores pueden incluir valores predeterminados para sus parámetros, lo que permite crear instancias sin necesidad de proporcionar todos los argumentos:

class Timer {
  seconds: number;
  running: boolean;
  
  constructor(seconds: number = 60, running: boolean = false) {
    this.seconds = seconds;
    this.running = running;
  }
  
  start(): void {
    this.running = true;
  }
}

// Usando valores predeterminados
const defaultTimer = new Timer();
console.log(defaultTimer.seconds); // 60

// Proporcionando valores personalizados
const customTimer = new Timer(30, true);
console.log(customTimer.seconds); // 30
console.log(customTimer.running); // true

Parámetros opcionales

También podemos definir parámetros opcionales usando el operador ?:

class Notification {
  message: string;
  type: string;
  duration: number | null;
  
  constructor(message: string, type: string, duration?: number) {
    this.message = message;
    this.type = type;
    this.duration = duration !== undefined ? duration : null;
  }
}

// Sin el parámetro opcional
const alert = new Notification("Server error", "error");
console.log(alert.duration); // null

// Con el parámetro opcional
const toast = new Notification("File saved", "success", 3000);
console.log(toast.duration); // 3000

Inicialización de propiedades en el constructor

El constructor es el lugar ideal para realizar inicializaciones complejas o cálculos basados en los parámetros recibidos:

class Order {
  id: string;
  items: string[];
  total: number;
  createdAt: Date;
  
  constructor(items: string[], prices: number[]) {
    this.id = `ORD-${Date.now()}`;
    this.items = [...items]; // Copia defensiva
    this.total = prices.reduce((sum, price) => sum + price, 0);
    this.createdAt = new Date();
  }
}

const order = new Order(["Laptop", "Mouse"], [1200, 25]);
console.log(order.id); // "ORD-1234567890"
console.log(order.total); // 1225

Sintaxis abreviada de parámetros

TypeScript ofrece una sintaxis abreviada para declarar e inicializar propiedades directamente en los parámetros del constructor, utilizando modificadores de acceso:

class User {
  // No es necesario declarar las propiedades por separado
  constructor(
    public username: string,
    public email: string,
    private password: string,
    readonly createdAt: Date = new Date()
  ) {
    // El cuerpo puede estar vacío o contener lógica adicional
  }
  
  validatePassword(input: string): boolean {
    return input === this.password;
  }
}

const user = new User("johndoe", "john@example.com", "secret123");
console.log(user.username); // "johndoe"
console.log(user.email); // "john@example.com"
// console.log(user.password); // Error: 'password' es privado
console.log(user.createdAt); // Date actual

Esta sintaxis es equivalente a declarar las propiedades fuera del constructor y luego asignarles valores, pero resulta más concisa y menos propensa a errores.

Sobrecarga de constructores

TypeScript permite definir múltiples firmas para el constructor, lo que posibilita crear instancias de diferentes maneras:

class Point {
  x: number;
  y: number;
  
  // Sobrecargas (solo firmas)
  constructor();
  constructor(obj: {x: number, y: number});
  constructor(x: number, y: number);
  
  // Implementación real que maneja todos los casos
  constructor(xOrObj?: number | {x: number, y: number}, y?: number) {
    if (typeof xOrObj === 'object') {
      this.x = xOrObj.x;
      this.y = xOrObj.y;
    } else if (typeof xOrObj === 'number' && typeof y === 'number') {
      this.x = xOrObj;
      this.y = y;
    } else {
      this.x = 0;
      this.y = 0;
    }
  }
}

// Diferentes formas de crear instancias
const p1 = new Point(); // {x: 0, y: 0}
const p2 = new Point({x: 10, y: 20}); // {x: 10, y: 20}
const p3 = new Point(5, 15); // {x: 5, y: 15}

Constructores y herencia

Cuando trabajamos con herencia, el constructor de una clase derivada debe llamar al constructor de la clase base usando super():

class Vehicle {
  make: string;
  model: string;
  
  constructor(make: string, model: string) {
    this.make = make;
    this.model = model;
  }
}

class Car extends Vehicle {
  doors: number;
  
  constructor(make: string, model: string, doors: number) {
    // Debe llamar a super() antes de acceder a 'this'
    super(make, model);
    this.doors = doors;
  }
}

const myCar = new Car("Toyota", "Corolla", 4);
console.log(`${myCar.make} ${myCar.model}, ${myCar.doors} doors`);
// "Toyota Corolla, 4 doors"

La llamada a super() debe realizarse antes de acceder a this en el constructor de la clase derivada.

Constructores privados

Los constructores pueden marcarse como privados para controlar cómo se crean las instancias de una clase:

class Database {
  private static instance: Database | null = null;
  private connectionString: string;
  
  // Constructor privado
  private constructor(connectionString: string) {
    this.connectionString = connectionString;
    console.log("Connecting to database...");
  }
  
  // Método estático para obtener la instancia
  static connect(connectionString: string): Database {
    if (!Database.instance) {
      Database.instance = new Database(connectionString);
    }
    return Database.instance;
  }
  
  query(sql: string): void {
    console.log(`Executing query: ${sql}`);
  }
}

// No se puede usar: const db = new Database("..."); // Error

// Forma correcta de obtener una instancia
const db = Database.connect("postgres://localhost:5432/mydb");
db.query("SELECT * FROM users");

Este patrón (Singleton) garantiza que solo exista una instancia de la clase en toda la aplicación.

Ejemplo práctico: Creación de un sistema de tareas

Veamos un ejemplo más completo que utiliza constructores con diferentes características:

class Task {
  id: string;
  title: string;
  description: string;
  priority: 'low' | 'medium' | 'high';
  completed: boolean;
  createdAt: Date;
  
  constructor(
    title: string,
    description: string = "",
    priority: 'low' | 'medium' | 'high' = 'medium'
  ) {
    this.id = Math.random().toString(36).substr(2, 9);
    this.title = title;
    this.description = description;
    this.priority = priority;
    this.completed = false;
    this.createdAt = new Date();
  }
}

class TaskManager {
  private tasks: Task[] = [];
  
  constructor(initialTasks: Task[] = []) {
    this.tasks = [...initialTasks];
  }
  
  addTask(task: Task): void {
    this.tasks.push(task);
  }
  
  createTask(title: string, description?: string, priority?: 'low' | 'medium' | 'high'): Task {
    const newTask = new Task(title, description, priority);
    this.addTask(newTask);
    return newTask;
  }
  
  getTaskById(id: string): Task | undefined {
    return this.tasks.find(task => task.id === id);
  }
}

// Uso del sistema
const manager = new TaskManager();
const task1 = manager.createTask("Implement login", "Create login form with validation", "high");
const task2 = manager.createTask("Write documentation");

console.log(task1.priority); // "high"
console.log(task2.priority); // "medium" (valor predeterminado)

En este ejemplo, el constructor de Task genera un ID único, establece valores predeterminados y configura el estado inicial. El constructor de TaskManager acepta una lista opcional de tareas iniciales.

Mejores prácticas para constructores

  • 1. Mantén los constructores simples: Enfócate en inicializar propiedades. La lógica compleja debe ir en métodos separados.
class ShoppingCart {
  items: Array<{product: string, price: number}> = [];
  
  constructor(initialItems: Array<{product: string, price: number}> = []) {
    this.items = [...initialItems];
  }
  
  // Lógica compleja en métodos separados
  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}
  • 2. Usa parámetros con valores predeterminados para simplificar la creación de instancias:
class Configuration {
  constructor(
    public debug: boolean = false,
    public timeout: number = 30000,
    public retries: number = 3
  ) {}
}

// Fácil de usar con configuración predeterminada
const defaultConfig = new Configuration();

// O con valores personalizados
const customConfig = new Configuration(true, 60000);
  • 3. Considera usar patrones de diseño como Factory o Builder para casos complejos:
class UserBuilder {
  private user: {
    name: string;
    email?: string;
    age?: number;
    role?: string;
  };
  
  constructor(name: string) {
    this.user = { name };
  }
  
  withEmail(email: string): UserBuilder {
    this.user.email = email;
    return this;
  }
  
  withAge(age: number): UserBuilder {
    this.user.age = age;
    return this;
  }
  
  withRole(role: string): UserBuilder {
    this.user.role = role;
    return this;
  }
  
  build(): any {
    return { ...this.user };
  }
}

// Uso del patrón Builder
const user = new UserBuilder("John Doe")
  .withEmail("john@example.com")
  .withAge(30)
  .withRole("admin")
  .build();

Los constructores y sus parámetros son elementos fundamentales en TypeScript para crear objetos con estados iniciales bien definidos. La flexibilidad que ofrecen los parámetros opcionales, valores predeterminados y la sintaxis abreviada permite diseñar APIs de clase intuitivas y fáciles de usar, mientras que el sistema de tipos garantiza que los objetos se creen correctamente.

Diferencias con clases en JavaScript

TypeScript extiende la sintaxis de clases de JavaScript con características adicionales de tipado estático y otras mejoras que facilitan el desarrollo orientado a objetos. Aunque las clases en ambos lenguajes comparten una base común, existen diferencias significativas que todo desarrollador debe conocer.

Sistema de tipos

La diferencia más evidente entre las clases de TypeScript y JavaScript es el sistema de tipos:

// TypeScript
class Product {
  id: number;
  name: string;
  price: number;
  
  constructor(id: number, name: string, price: number) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
}

// JavaScript
class Product {
  constructor(id, name, price) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
}

En TypeScript, las propiedades deben declararse con sus tipos antes de usarse, mientras que JavaScript no requiere esta declaración explícita.

Modificadores de acceso

TypeScript incorpora modificadores de acceso (public, private, protected) que no existen nativamente en JavaScript:

// TypeScript
class BankAccount {
  public owner: string;
  private balance: number;
  protected accountNumber: string;
  
  constructor(owner: string, initialBalance: number) {
    this.owner = owner;
    this.balance = initialBalance;
    this.accountNumber = `ACC-${Math.floor(Math.random() * 10000)}`;
  }
  
  deposit(amount: number): void {
    this.balance += amount;
  }
}

const account = new BankAccount("Alice", 1000);
console.log(account.owner); // OK
// console.log(account.balance); // Error: Property 'balance' is private

JavaScript (hasta ES2022) no tiene modificadores de acceso nativos, aunque existe una propuesta para campos privados usando el prefijo #:

// JavaScript moderno (ES2022+)
class BankAccount {
  #balance;
  
  constructor(owner, initialBalance) {
    this.owner = owner;
    this.#balance = initialBalance;
  }
  
  deposit(amount) {
    this.#balance += amount;
  }
}

Propiedades readonly

TypeScript ofrece el modificador readonly para propiedades que no deben modificarse después de su inicialización:

class Vehicle {
  readonly vin: string;
  model: string;
  
  constructor(vin: string, model: string) {
    this.vin = vin;
    this.model = model;
  }
  
  updateInfo(newModel: string) {
    // this.vin = "NEW-VIN"; // Error: Cannot assign to 'vin' because it is a read-only property
    this.model = newModel; // OK
  }
}

JavaScript no tiene un equivalente nativo para readonly.

Parámetros de propiedades

TypeScript permite una sintaxis abreviada para declarar e inicializar propiedades directamente en el constructor:

// TypeScript - sintaxis abreviada
class User {
  constructor(
    public username: string,
    private password: string,
    readonly createdAt: Date = new Date()
  ) {}
  
  validatePassword(input: string): boolean {
    return input === this.password;
  }
}

// Equivalente en JavaScript
class User {
  constructor(username, password) {
    this.username = username;
    this.password = password;
    this.createdAt = new Date();
  }
  
  validatePassword(input) {
    return input === this.password;
  }
}

Esta característica reduce significativamente el código repetitivo en clases con muchas propiedades.

Interfaces y verificación de tipos

TypeScript permite que las clases implementen interfaces, garantizando que cumplan con una estructura específica:

interface Printable {
  print(): string;
}

class Document implements Printable {
  constructor(private content: string) {}
  
  print(): string {
    return this.content;
  }
}

class Image implements Printable {
  constructor(private url: string) {}
  
  print(): string {
    return `Image at ${this.url}`;
  }
}

function printDocument(doc: Printable) {
  console.log(doc.print());
}

printDocument(new Document("Hello World")); // OK
printDocument(new Image("photo.jpg"));      // OK

JavaScript no tiene interfaces ni verificación de tipos en tiempo de compilación.

Genéricos en clases

TypeScript soporta clases genéricas, permitiendo crear componentes reutilizables que funcionan con diferentes tipos:

class Queue<T> {
  private items: T[] = [];
  
  enqueue(item: T): void {
    this.items.push(item);
  }
  
  dequeue(): T | undefined {
    return this.items.shift();
  }
  
  peek(): T | undefined {
    return this.items[0];
  }
}

const numberQueue = new Queue<number>();
numberQueue.enqueue(10);
numberQueue.enqueue(20);

const stringQueue = new Queue<string>();
stringQueue.enqueue("hello");

JavaScript no tiene soporte nativo para genéricos.

Decoradores

TypeScript tiene soporte para decoradores que permiten añadir metadatos o modificar el comportamiento de clases y sus miembros:

function log(target: any, propertyKey: string) {
  const originalMethod = target[propertyKey];
  
  target[propertyKey] = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with:`, args);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(5, 3); // Logs: "Calling add with: [5, 3]" and returns 8

JavaScript tiene una propuesta de decoradores, pero su implementación y sintaxis difieren de TypeScript.

Comprobación de inicialización de propiedades

TypeScript verifica que todas las propiedades estén inicializadas:

class Form {
  // Error: Property 'name' has no initializer and is not definitely assigned in the constructor
  name: string;
  
  // OK: Inicializada en la declaración
  email: string = "";
  
  // OK con el operador !
  phone!: string;
  
  constructor() {
    // Si inicializamos name aquí, no habría error
  }
}

Para evitar este error, podemos:

  1. Inicializar la propiedad en su declaración
  2. Inicializarla en el constructor
  3. Usar el operador ! para indicar que será inicializada más tarde
  4. Marcarla como opcional con ?

JavaScript no realiza estas comprobaciones.

Sobrecarga de métodos

TypeScript permite definir sobrecargas de métodos para proporcionar diferentes firmas:

class Formatter {
  // Sobrecargas
  format(value: string): string;
  format(value: number, precision?: number): string;
  format(value: Date): string;
  
  // Implementación real
  format(value: string | number | Date, precision?: number): string {
    if (typeof value === "string") {
      return value.trim();
    } else if (typeof value === "number") {
      return value.toFixed(precision || 2);
    } else {
      return value.toISOString();
    }
  }
}

const formatter = new Formatter();
formatter.format("  hello  "); // "hello"
formatter.format(123.456);     // "123.46"
formatter.format(123.456, 0);  // "123"
formatter.format(new Date());  // "2023-05-15T14:30:00.000Z"

JavaScript no tiene soporte nativo para sobrecarga de métodos.

Campos estáticos privados y protegidos

TypeScript permite modificadores de acceso en miembros estáticos:

class ApiClient {
  private static apiKey: string;
  protected static baseUrl: string = "https://api.example.com";
  
  static initialize(key: string): void {
    ApiClient.apiKey = key;
  }
  
  static getAuthHeader(): Record<string, string> {
    return { Authorization: `Bearer ${ApiClient.apiKey}` };
  }
}

ApiClient.initialize("secret-key");
// console.log(ApiClient.apiKey); // Error: Property 'apiKey' is private

JavaScript solo permite campos estáticos públicos o privados con #.

Compatibilidad y transpilación

Una diferencia práctica importante es que el código de clases de TypeScript debe transpilarse a JavaScript antes de ejecutarse:

// TypeScript
class Person {
  private age: number;
  
  constructor(public name: string, age: number) {
    this.age = age;
  }
  
  isAdult(): boolean {
    return this.age >= 18;
  }
}

// JavaScript transpilado (simplificado)
var Person = /** @class */ (function () {
  function Person(name, age) {
    this.name = name;
    this.age = age;
  }
  Person.prototype.isAdult = function () {
    return this.age >= 18;
  };
  return Person;
}());

El compilador de TypeScript transforma las características no soportadas por JavaScript en código equivalente que funciona en el entorno de destino.

Ejemplo práctico: Comparación de implementaciones

Veamos un ejemplo completo que ilustra las diferencias clave:

// TypeScript
interface ShapeInterface {
  calculateArea(): number;
  getDescription(): string;
}

abstract class Shape implements ShapeInterface {
  constructor(protected readonly color: string) {}
  
  abstract calculateArea(): number;
  
  getDescription(): string {
    return `A ${this.color} shape with area ${this.calculateArea()} square units`;
  }
}

class Circle extends Shape {
  constructor(
    color: string,
    private radius: number
  ) {
    super(color);
  }
  
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
  
  getDescription(): string {
    return `A ${this.color} circle with radius ${this.radius}`;
  }
}

// JavaScript equivalente
class Shape {
  constructor(color) {
    this.color = color;
  }
  
  calculateArea() {
    throw new Error("Method not implemented");
  }
  
  getDescription() {
    return `A ${this.color} shape with area ${this.calculateArea()} square units`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
  
  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }
  
  getDescription() {
    return `A ${this.color} circle with radius ${this.radius}`;
  }
}

En este ejemplo, TypeScript proporciona:

  • Interfaces para garantizar la implementación correcta
  • Clases abstractas que no pueden instanciarse directamente
  • Modificadores de acceso (protected, private)
  • Propiedades de solo lectura (readonly)
  • Verificación de tipos en tiempo de compilación

Estas diferencias hacen que TypeScript sea una opción más robusta para proyectos orientados a objetos complejos, mientras que JavaScript ofrece mayor flexibilidad pero menos seguridad de tipos.

Aprende TypeScript online

Otros ejercicios de programación de TypeScript

Evalúa tus conocimientos de esta lección Clases y objetos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

Todas las lecciones de TypeScript

Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Typescript

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  1. Comprender la sintaxis básica para declarar una clase en TypeScript.
  2. Conocer cómo definir propiedades y métodos dentro de una clase.
  3. Entender el uso del constructor para inicializar objetos al crear instancias de la clase.
  4. Aprender a crear y utilizar métodos para que los objetos realicen operaciones específicas.
  5. Saber cómo crear instancias de una clase usando la palabra clave new.
  6. Familiarizarse con los modificadores de acceso (public, private y protected) y cómo afectan la accesibilidad de los miembros de la clase.
  7. Practicar la creación de clases y objetos en TypeScript para organizar y estructurar el código de manera eficiente y reutilizable.