Categorías en TypeScript

El JavaScript tradicional se basa en funciones y herencia de prototipos para crear componentes reutilizables, y algunos programadores pueden encontrar este método extraño y engorroso, especialmente aquellos familiarizados con la programación orientada a objetos basada en clases que heredan la funcionalidad de las clases base y están integradas en ellas. estas clases. 

A partir de ECMAScript 2015, también conocido como ECMAScript 6, los programadores de JavaScript pueden crear aplicaciones utilizando programación basada en clases orientada a objetos. TypeScript ahora permite a los desarrolladores usar estas tecnologías y traducirlas a JavaScript que funciona en todas las principales plataformas y navegadores, sin tener que esperar a que la próxima versión de JavaScript sea compatible.

Tabla de contenidos


Categorías

Veamos un ejemplo de clase simple:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

La sintaxis debería resultar familiar si ha utilizado lenguajes como C# o Java. Aquí estamos declarando una nueva clase llamada Greeter. Esta clase tiene tres componentes: una propiedad nominal greeting, función constructora y una continuación de greet. Notarás que usamos el prefijo this.‎ para acceder los elementos de la clase, lo que indica que es un acceso de miembro.

En la última línea, construimos una instancia de la clase Greeter usando la palabra clave new. Esto llama al constructor que definimos anteriormente, que crea un nuevo objeto con la forma de la clase, Greeter implementando el constructor para inicializarlo.

Herencia

Podemos usar los patrones comunes de programación orientada a objetos en TypeScript. Uno de los patrones básicos es la escalabilidad de clases para crear nuevas usando herencia. Veamos un ejemplo sencillo:

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

Este ejemplo demuestra la característica más básica de la herencia: que las clases heredan propiedades y métodos de las clases base. Aquí, la Dog clase es una clase derivada que se deriva de la clase base Animal con la palabra clave extends. Las clases derivadas generalmente se denominan «subclases» y las clases base se denominan «superclases».

Debido a que la clase Dog amplía la funcionalidad de la clase Animal, pudimos crear una instancia de la clase Dog que tiene dependientes bark()‎move()‎. Pasemos ahora a un ejemplo más complejo:

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

Este ejemplo cubre algunas otras características que no mencionamos anteriormente. Nuevamente usamos la palabra clave extends para crear dos subclases de la clase AnimalHorseSnake.

Una de las diferencias entre el ejemplo anterior y el anterior a este es que para cada clase derivada, su constructor debe tener una llamada al interruptor super()‎ que ejecuta el constructor de la clase base. Además, se debe realizar una llamada super()‎ antes de que se pueda acceder a una propiedad mediante this‎ un constructor dentro de un constructor. Esta es una regla general importante para TypeScript.

Este ejemplo también muestra cómo anular los métodos de la clase base y reemplazarlos con métodos personalizados de la subclase. Tanto la clase Snake como la clase Horse crean una función nombrada move después de que se une y anula la función move en la clase Animal, lo que le da a la función una función especial en cada clase. Tenga en cuenta que se tom declara que es de tipo Animal, y debido a que su valor es Horse, llamar a ‎tom.‎move‎(34)‎ la función de anulación de la clase Horse.

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

Determinantes public, private y protected

Todo es público por defecto

En los ejemplos anteriores, pudimos acceder libremente a los elementos que declaramos en los programas en todas partes. Si tiene experiencia trabajando con clases en otros lenguajes, puede notar que no necesitamos usar la palabra clave public en los ejemplos anteriores para hacer públicos los elementos, ya que C# requiere que la palabra clave public preceda claramente a los elementos para que sea visible. 

En TypeScript, todos los elementos son públicos de forma predeterminada. Pero aún puede marcar un elemento con la palabra public explícitamente. La clase Animal anterior de la siguiente manera:

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

Determinante private

Cuando el selector especifica un elemento private, significa que es un elemento privado al que no se puede acceder desde fuera de la clase que la contiene, por ejemplo:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name;
// Falso, elemento 'name' privado

TypeScript se basa en un sistema de tipo estructural. Cuando comparamos dos tipos diferentes, diremos que son compatibles si los tipos de todos los elementos son compatibles, independientemente de su origen.

Pero cuando comparamos dos tipos que contienen elementos privados ( private) y elementos protegidos ( protected), tratamos estos dos tipos como diferentes. Para que dos especies sean compatibles, una de ellas tiene un elemento especial, la otra debe tener un elemento especial que tenga su raíz en la misma declaración. El mismo principio se aplica a los artículos protegidos. Veamos un ejemplo donde ilustra:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // Error
// Tipos de error 'Animal' y 'Employee' no son compatibles

En este ejemplo, tenemos class Animal y class RhinoRhino siendo una subclase de class Animal. También tenemos una nueva variedad con un nombre Employee similar a la variedad Animal en su forma. Hacemos copias de estas clases e intentamos asignarlas entre sí para ver qué sucede. Debido AnimalRhino comparten el lado privado de su formulario de la misma ‎private‎ name:‎ string‎‎ declaración dentro de la clase Animal, son compatibles. 

Pero la clase Employee no existe en el mismo estado. Obtenemos un error cuando intentamos asignar una instancia de la clase Employee a una instancia de la clase Animal, lo que nos dice que los dos tipos no son compatibles. Aunque la clase Employee tiene un elemento llamado name, este elemento no es el mismo que el de la clase Animal.

Determinante protected

Un selector se comporta protected como selector private excepto que los elementos declarados como protegidos por el selector protected son accesibles desde las clases derivadas, por ejemplo:

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name);  // Error

Tenga en cuenta que aunque no podemos acceder al elemento name desde fuera de la clase Person, aún podemos usarlo desde dentro del método de instancia de la clase Employee porque la clase Employee hereda de la clase Person.

El constructor también se puede marcar con el determinante protected. Esto significa que no se puede crear una instancia de una clase, es decir, no se puede crear una copia fuera de la clase que contiene, pero se puede extender (es decir heredar), por ejemplo:

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}
// La clase Employee puede extender la clase Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); //Error, no es posible crear una instancia de la clase porque su constructor esta protegido

Modificador de solo lectura

Puede marcar una propiedad como de solo lectura mediante el uso de readonly. Las propiedades de solo lectura deben inicializarse cuando se declaran o dentro del constructor.

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit";  // Falso, la propiedad es de solo lectura, no se le puede asignar un valor

Propiedades de parámetro

En el ejemplo anterior, tuvimos que declarar un elemento de solo lectura con el nombre name y argumento del constructor como theName y en la clase Octopus luego asignar inmediatamente un valor al theName y name. Esta forma de trabajar es muy común. Entonces, las propiedades de los parámetros son una característica que nos permite crear e inicializar un objeto en un solo lugar. La siguiente es una revisión de la clase Octopus usando la propiedad de parámetro:

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

Observe cómo renunciamos por completo al uso de theName y usamos la declaración corta como un ‎readonly‎ name:‎ string‎ ‎parámetro para el constructor crear e inicializar el name. Hemos recopilado las declaraciones con la cita en un solo lugar.

Las propiedades de los parámetros se declaran prefijando los parámetros del constructor con un especificador de acceso o una palabra clave readonly, o ambos. Usando una propiedad private de parámetro que declara e inicializa un elemento privado. El mismo principio se aplica a publicprotectedreadonly.

Funciones de acceso

TypeScript admite getters que obtienen valores de propiedad y setters que establecen valores de propiedad, una característica utilizada para modificar la forma en que los programas se comportan al acceder a los elementos de un objeto. Esto le brinda una mejor manera de controlar cómo se accede a los elementos en cada objeto. Convirtamos una clase simple para usar las palabras clave getset. Primero, comencemos con un ejemplo sin las funciones fetch y set:

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

Permitir que las personas establezcan un valor para la propiedad fullName que representa el nombre completo del empleado hace que el trabajo sea más fácil, pero dejar que las personas cambien los nombres tan simple como eso puede causarnos problemas.

En la versión a continuación, verificamos que haya una cadena secreta ( variable passcode) antes de permitir que se modifique el empleado. Hacemos esto reemplazando el acceso directo a la propiedad con un fullName método set que verifica la existencia y corrección de la cadena secreta antes de cambiar el nombre. Agregamos una función get para permitir que ejemplo anterior se ejecute como estaba:

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

Para comprobar que el método de acceso ahora está comprobando la frase secreta, se puede cambiar y ver si hay un mensaje avisandonos de que no tenemos privilegios para actualizar los datos de los empleados. Hay algunas cosas sobre la accesibilidad a considerar:

  • Los métodos de acceso requieren que el compilador esté configurado para generar código que admita ECMAScript versión 5 o posterior de JavaScript. No es posible cambiar a ECMAScript 3.
  • Cuando la función de acceso se usa get sin la función de acceso, set significa que la propiedad readonly es de solo lectura automáticamente . Esto es útil al generar un archivo ‎.d.ts‎ a partir de código, porque los usuarios de propiedades notarán que no pueden cambiar su valor.

Propiedades estáticas (static)

Hasta ahora, solo hemos hablado de elementos de instancia en la clase, que son los que aparecen cuando se inicializa un objeto. Pero también podemos crear elementos estáticos , que son elementos que aparecen en la propia clase en lugar de en las copias. En este ejemplo, usamos la palabra clave static en el punto de origen origin porque es un valor global para todas las cuadrículas. 

Para que cada copia alcance este valor anteponiéndole como prefijo el nombre de la clase. Así como usamos el prefijo ‎this ‎cuando accedemos a datos de copia, usamos el prefijo para ‎Grid.‎ acceder datos estáticos.

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

Clases abstractas

Las clases abstractas son clases base de las que se pueden derivar otras clases. Es posible que no se formatee directamente. A diferencia de las interfaces, las clases abstractas pueden contener detalles de implementación de sus elementos. La palabra clave se utiliza abstract para definir clases abstractas y funciones abstractas dentro de una clase abstracta:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

Los métodos abstractos dentro de una clase abstracta no tienen implementación y deben aplicarse a la clase derivada. La estructura de las funciones abstractas es similar a la de las funciones de interfaz, ya que ambas definen la firma de la función sin su cuerpo (el código que está dentro de la función). Sin embargo, los métodos abstractos deben estar precedidos por la palabra clave abstract, y opcionalmente pueden contener modificadores de acceso:

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log("Department name: " + this.name);
    }

    abstract printMeeting(): void;  // debe aplicarse a las clases derivadas 
}

class AccountingDepartment extends Department {

    constructor() {
        // Las funciones constructoras dentro de las clases derivadas deben llamar 
        // super()
        super("Accounting and Auditing");
    }

    printMeeting(): void {
        console.log("The Accounting Department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}

let department: Department; // Se permite crear una referencia a un tipo abstracto.
department = new Department(); // Error, no se puede crear una instancia de una clase abstracta
department = new AccountingDepartment(); // Cree una copia de una subclase no abstracta y asígnela a una variable permitida
department.printName();
department.printMeeting();
department.generateReports(); // Error, la función no existe en la declaración de tipo abstracto

Tecnologías avanzadas

Funciones del constructor

Al declarar una clase en TypeScript, en realidad está creando varias declaraciones al mismo tiempo. La primera es la instancia de la clase.

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

En el ejemplo anterior ‎let greeter: Greeter‎, usamos la declaración de clase Greeter como el tipo de copia de clase Greeter. Esto es muy familiar para los programadores familiarizados con otros lenguajes orientados a objetos.

También creamos otro valor que llamamos la función constructora. Esta función se llama al crear instancias de la clase con la palabra clave new. Veamos el código JavaScript generado por el ejemplo anterior para ver cómo funciona esto en la práctica:

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

La declaración let Greeter aquí será asignada al constructor. Obtenemos una instancia de la clase cuando se llama y ejecuta esta función con la palabra clave new. El constructor también contiene todos los elementos de la clase estática. Cada clase puede verse como si tuviera un lado de copia y un lado estático. Modifiquemos un poco el ejemplo para mostrar la diferencia:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

En este ejemplo,  greeter1 funciona como antes. Creamos una instancia de Greeter y usamos este objeto. Hemos visto esto antes. Entonces usamos la clase directamente. Aquí creamos una nueva variable llamada greeterMaker. Esta variable llevará la misma clase, o mejor dicho, su constructor. Aquí usamos «dame el mismo tipo de clase» typeof Greeter como si fuéramos a decir «dame el mismo tipo de clase Greeter» en lugar del tipo de copia. 

O más precisamente, como si dijéramos «dame el tipo de símbolo nombrado por el nombre Greeter» que es el tipo del constructor. Este tipo contiene todos los elementos estáticos de la clase Greeter así como el constructor que crea las instancias de la clase Greeter. Mostramos esto usando la palabra clave new en greeterMaker creando nuevas versiones y usándolo como antes.

Utilizar una clase como interfaz

Como dijimos antes, declarar una clase crea dos cosas: un tipo que representa la instancia de la clase y un constructor. Debido a que las clases crean tipos, se pueden usar en los mismos lugares que las interfaces.

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

Recursos del Artículo


Deja un comentario