Orientación a objetos

<< Tipado estático Tipado avanzado >>

La programación orientada a objetos en TypeScript es igual que la POO en JavaScript, salvo algunos añadidos, basados sobre todo en las posibilidades de tipado del lenguaje, que lo ponen a la altura de lenguajes como Java o C#. A continuación vamos a ver las diferencias respecto a JavaScript.

Tipado de atributos y métodos

La primera diferencia es que TypeScript obliga a declarar los atributos de la clase fuera del constructor. De esta forma tipamos dichos atributos. Opcionalmente se le puede poner un valor por defecto.

class Persona {
    nombre: string;
    edad: number;

    constructor(nombre: string, edad: number) {
        this.nombre = nombre;
        this.edad = edad;
    }
}

Ámbito público y privado

En TypeScript existen los modificadores de ámbito private y public. Sin embargo, desde hace poco tiempo, también se ha introducido en JavaScript el prefijo '#' para indicar que un atributo o método es privado (el resto es público). ¿Cuál es mejor usar en TypeScript?.

private vs #

Aunque usar private es perfectamente válido para que te avise de "casi" todos los errores que tengan que ver con acceder directamente a atributos (o métodos) privados desde fuera de la clase, tiene un problema. A la hora de compilar a JavaScript esos modificadores se eliminan (no existen en JavaScript), por lo que va a funcionar como si fueran públicas.

class Persona {
    nombre: string;
    edad: number;
    private rol: string;

    constructor(nombre: string, edad: number, rol: string) {
        this.nombre = nombre;
        this.edad = edad;
        this.rol = rol;
    }
}

const p = new Persona("Juan", 42, "admin");
console.log(p.rol); // Property 'rol' is private and only accessible within class 'Persona'
Object.entries(p).forEach(([k,v]) => console.log(`${k} => ${v}`)); // Recorremos las propiedades del objeto
/*
nombre => Juan
edad => 42
rol => admin -> Estamos accediendo a una propiedad "private". En JavaScript se elimina el modificador
*/

Usando el modificador '#' de JavaScript, estos atributos siguen siendo privados después de compilar a JavaScript, por lo que un acceso ilegal producirá error. De esta manera, sería recomendable usar este modificador si queremos que algo sea privado de verdad. Hasta hace poco, usar private era la única opción existente.

class Persona {
    nombre: string;
    edad: number;
    #rol: string;

    constructor(nombre: string, edad: number, rol: string) {
        this.nombre = nombre;
        this.edad = edad;
        this.#rol = rol;
    }
}

const p = new Persona("Juan", 42, "admin");
//console.log(p.#rol); // roperty '#rol' is not accessible outside class 'Persona' because it has a private identifier
Object.entries(p).forEach(([k,v]) => console.log(`${k} => ${v}`)); // No va a listar el atributo #rol
/*
nombre => Juan
edad => 42
*/

public y private en parámetros del constructor

TypeScript también permite pasarle al constructor parámetros con el modificador public o private. En este caso no hay que declarar esos atributos fuera del constructor y TypeScript asigna el valor del parámetro al atributo automáticamente. Estos 2 ejemplos a continuación son equivalentes:

class Persona {
    constructor(public nombre: string, public edad: number) { }
}

const p = new Persona("Juan", 34);
console.log(`${p.nombre} - ${p.edad}`); // Juan - 34

Lo malo de esto es que si tenemos atributos que queramos que sean privados, no podemos usar el modificador de JavaScript '#' de esta manera, perdiendo sus ventajas. En su lugar utilizaremos el modificador private.

Interfaces

En TypeScript, las interfaces tienen 2 usos principales. El primero es el clásico de otros lenguajes de programación, obligar a la clase que implementa dicha interfaz a implementar ciertos métodos, o incluso ciertos atributos. El segundo sería para tipar objetos (JSON o instancias de clases) y establecer qué atributos deben tener.

Implementar interfaces en clases

Al igual que ocurre con otros lenguajes como Java o C#, podemos crear interfaces donde definimos métodos sin implementar, solo tipados. De esta manera, las clases que implementen una interfaz, están obligadas a implementar dichos métodos. También se pueden definir atributos que la clase que implementa la interfaz también deberá tener.

interface Saluda {
    saluda: () => void
}

class Persona implements Saluda {
    //Resto del código

    // Obligatorio implementar este método
    saluda(): void {
        console.log(`Hola, me llamo ${this.nombre}`);
    }
}

Interfaces para tipar objetos

En TypeScript se suelen utilizar mucho interfaces que solo contienen atributos para tipar variables, arrays, etc. Si tipamos usando una interfaz quiere decir, que el valor debe ser un objeto que tenga dichos atributos. Puede ser un objeto JSON con las mismas propiedades, o un objeto instanciado a partir de una clase que también tenga esas propiedades (o que implemente la interfaz).

interface Direccion {
    calle: string;
    numero: number;
    cp: string;
}

interface Persona {
    nombre: string;
    edad: number;
    direccion: Direccion;
    telefonos: string[];
}

const p: Persona = {
    nombre: "Pedro",
    edad: 35,
    direccion: {
        calle: "Perico Palotes",
        numero: 12,
        cp: "24353"
    },
    telefonos: ["9542345453", "6574352643"]
};
console.log(p);

Polimorfismo

En los lenguajes con orientación a objetos estáticamente tipados existe el concepto de Polimorfismo, y está ligado a la herencia. Este implica que a la hora de establecer el tipo para un objeto, se puede utilizar una clase base o una interfaz que implemente la clase del objeto. En definitiva, un tipo más genérico que contenga algunos de los atributos o métodos del objeto.

class Persona {
    constructor(public nombre: string, public edad: number) { }
}

class Usuario extends Persona {
    constructor(nombre: string, edad: number, public email: string, public password: string) {
        super(nombre, edad);
    }
}

class Cliente extends Persona {
    constructor(nombre: string, edad: number, public vip: boolean) {
        super(nombre, edad);
    }
}

const p: Persona = new Usuario("Juan", 35, "juan@email.com", "1234");
const p2: Persona = new Cliente("Pepe", 64, true);
const personas: Persona[] = [p, p2];

La limitación de usar polimorfismo es la de no poder acceder a atributos o métodos del objeto que no estén definidos en el tipo usado. Para poder acceder a ellos, debemos usar primero un casting (conversión) de tipos explícito.

// Declaraciones de clases anteriores

const p: Persona = new Usuario("Juan", 35, "juan@email.com", "1234");

console.log(p.email); // Error: Property 'email' does not exist on type 'Persona'

const usuario = p as Usuario; // Casting explícito
console.log(usuario.email); // OK
console.log((p as Usuario).email); // También es válido

<< Tipado estático Tipado avanzado >>