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ícateDefinició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:
- Inicializar la propiedad en su declaración
- Inicializarla en el constructor
- Usar el operador
!
para indicar que será inicializada más tarde - 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.
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
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Proyecto Inventario de productos
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
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
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras Y Efectos Secundarios
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales Y Tipos Condicionales
Tipos Intermedios Y Avanzados
Tipos Genéricos Básicos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad (Partial, Required, Pick, Etc)
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la sintaxis básica para declarar una clase en TypeScript.
- Conocer cómo definir propiedades y métodos dentro de una clase.
- Entender el uso del constructor para inicializar objetos al crear instancias de la clase.
- Aprender a crear y utilizar métodos para que los objetos realicen operaciones específicas.
- Saber cómo crear instancias de una clase usando la palabra clave
new
. - Familiarizarse con los modificadores de acceso (
public
,private
yprotected
) y cómo afectan la accesibilidad de los miembros de la clase. - Practicar la creación de clases y objetos en TypeScript para organizar y estructurar el código de manera eficiente y reutilizable.