TypeScript : POO et espaces de noms

Je vous propose de continuer à explorer les possibilités de TypeScript en abordant la programmation orientée objet. JavaScript a une approche objet bien particulière avec les prototypes. TypeScript permet d’utiliser une approche plus classique, c’est ce que nous allons voir dans cet article.

Les classes

TypeScript introduit le mot-clé class pour créer une classe. Voici un premier exemple :

class User {
    public name: string;
    public age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;        
    }
    salut() {
        alert('Coucou ' + this.name + ' !');
    }
}

Si vous pratiquez des langages comme C#, Java, ou même PHP, vous ne devez pas être dépaysé avec la syntaxe.

On déclare la classe User avec class.

On définit deux propriétés en précisant le type :

  • name de type string,
  • age de type number.

On a ensuite un constructeur (constructor) avec deux paramètres qui permettent d’assigner les deux propriétés.

Pour finir on a une méthode (salut) qui permet de saluer l’utilisateur.

Pour utiliser cette classe et ainsi créer et utiliser un objet c’est aussi de la syntaxe classique :

let user = new User('Marcel', 35);
user.salut();

Voyons le code JavaScript généré pour tout ça :

var User = (function () {
    function User(name, age) {
        this.name = name;
        this.age = age;
    }
    User.prototype.salut = function () {
        alert('Coucou ' + this.name + ' !');
    };
    return User;
}());
var user = new User('Marcel', 35);
user.salut();

C’est quand même plus lourd avec prototype !

Notez qu’on peut déclarer une classe abstraite, qui ne peut donc pas être instanciée, avec abstract.

Héritage

Voyons maintenant comment faire de l’héritage. On va poursuivre l’exemple précédent :

class User {
  public name: string;
  public age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;		
  }
  salut() {
    alert('Coucou ' + this.name + ' !');
  }
}
class Redactor extends User {
  public category: string;
  constructor(name: string, age: number, category: string) {
    super(name, age);
    this.category = category;
  }
  salut() {
    alert('Coucou ' + this.name + ' de la catégorie ' + this.category + ' !');
  }
}
let redactor = new Redactor('Marcel', 35, 'Jeunesse');
redactor.salut();

J’ai créé une classe Redactor qui étend User avec extends. Dans le constructeur on utilise super pour faire appel à la classe parente.

On voit aussi qu’on peut surcharger une méthode, ici salut.

Si on regarde maintenant le JavaScript généré on se rend compte de l’intérêt de TypeScript :

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var User = (function () {
    function User(name, age) {
        this.name = name;
        this.age = age;
    }
    User.prototype.salut = function () {
        alert('Coucou ' + this.name + ' !');
    };
    return User;
}());
var Redactor = (function (_super) {
    __extends(Redactor, _super);
    function Redactor(name, age, category) {
        _super.call(this, name, age);
        this.category = category;
    }
    Redactor.prototype.salut = function () {
        alert('Coucou ' + this.name + ' de la catégorie ' + this.category + ' !');
    };
    return Redactor;
}(User));
var redactor = new Redactor('Marcel', 35, 'Jeunesse');
redactor.salut();

Les propriétés et les accesseurs

Dans toute bonne approche objet on doit pouvoir protéger les propriétés. Par défaut avec TypeScript elles sont publiques, donc totalement accessibles.

On dispose des possibilités classiques pour les protéger :

  • private : accessiblement uniquement dans la classe,
  • protected : comme private mais en plus accessible dans les classes héritées,
  • public : accessible partout,
  • static : accessible au niveau de la classe plutôt que d’une instance.

Mais évidemment si on protège les propriétés il faut prévoir des accesseurs pour les lire ou les modifier. Voici un exemple :

class User {
    protected _name: string;
    get name(): string {
        return this._name;
    }
    set name(newName: string) {
        if (newName.length < 11) {
            this._name = newName;
            alert('Le nom ' + newName + ' a bien été enregistré !')
        }
        else {
            console.log("Erreur: Le nom ne doit pas comporter plus de 10 caractères !");
        }
    }
}
let user = new User();
user.name = 'Un nom bien trop long'

On utilise set et get pour créer les accesseurs.

Remarquez qu’à la compilation vous allez rencontrer cette erreur :

error TS1056: Accessors are only available when targeting ECMAScript 5 and higher.

On vous prévient que le JavaScript généré ne fonctionnera que dans un navigateur qui supporte ES5, mais en fait c’est pratiquement la totalité des navigateurs existants.

Là aussi un coup d’oeil au code généré nous montre tout l’intérêt d’utiliser TypeScript :

var User = (function () {
    function User() {
    }
    Object.defineProperty(User.prototype, "name", {
        get: function () {
            return this._name;
        },
        set: function (newName) {
            if (newName.length < 11) {
                this._name = newName;
                alert('Le nom ' + newName + ' a bien été enregistré !');
            }
            else {
                alert("Erreur: Le nom ne doit pas comporter plus de 10 caractères !");
            }
        },
        enumerable: true,
        configurable: true
    });
    return User;
}());
var user = new User();
user.name = 'Un nom bien trop long';

Interface

La notion d’interface dans TypeScript s’éloigne un peu de ce qu’on connaît dans les autres langages, voyons cela avec un exemple :

function afficheNom(objetNomme: { name: string }) {
    alert(objetNomme.name);
}

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

let user = new User('Alfred',  32);
afficheNom(user);

Le paramètre de la fonction afficheNom doit être un objet avec une propriété nom de type string.

La classe User a bien cette propriété, on peut donc créer un objet et l’utiliser comme valeur à envoyer à la fonction. le fait que la classe possède d’autre propriétés n’est pas important, il suffit de trouver au moins la propriété requise.

On peut améliorer la définition du type du paramètre en utilisant une interface pour le définir :

interface ValeurNom {
    name: string;
}

function afficheNom(objetNomme: ValeurNom) {
    alert(objetNomme.name);
}

Le code est ainsi plus propre, on définit clairement ce qu’on veut avoir comme paramètre.

Il est possible d’avoir des propriétés optionnelles :

interface ValeurUser {
    name: string;
    category?: string;
}

function afficheUser(objetUser: ValeurUser) {
    alert(objetUser.name);
    if (objetUser.hasOwnProperty('category')) {
        alert(objetUser.category);
    }
}

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

class Redactor extends User {
    public category: string;
    constructor(name: string, age: number, category: string) {
        super(name, age);
        this.category = category;
    }
}

let user = new User('Alfred',  32);
afficheUser(user);
let redactor = new Redactor('Toto', 42, 'Roman');
afficheUser(redactor);

Ici dans l’interface name est requis mais category est optionnel. POur un simple utilisateur on affiche que le nom, mais pour un rédacteur on affiche également la catégorie.

Sans entrer dans les détails on peut aussi définir une fonction, un tableau, mais aussi quelque chose de plus classique : une classe :

interface UserInterface {
    name: string;
    age: number;
}

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

On voit donc que dans TypeScript la notion d’interface est plus étendue que ce à quoi on est habitués. En gros on définit la forme que doit prendre un élément, avec une grande souplesse.

Espaces de noms

Les espaces de noms sont fondamentaux pour éviter des conflits de nommage. TypeScript nous offre cette fonctionnalité !

namespace A {
  export function calcule(x: number, y:number) {
    return x + y;
  }
}
namespace B {
  export function calcule(x: number, y: number) {
    return x * y;
  }
}
alert(A.calcule(2, 3));
alert(B.calcule(2, 3));

On a deux espaces de noms : A et B. Dans chaque espace une fonction avec un nom identique : calcule. Avec export on peut utiliser les fonctions en dehors des espaces de noms. On a tout bien rangé !

Voici le JavaScript généré :

var A;
(function (A) {
    function calcule(x, y) {
        return x + y;
    }
    A.calcule = calcule;
})(A || (A = {}));
var B;
(function (B) {
    function calcule(x, y) {
        return x * y;
    }
    B.calcule = calcule;
})(B || (B = {}));
alert(A.calcule(2, 3));
alert(B.calcule(2, 3));

Une bonne nouvelle également c’est qu’un espace de nom peut être dispersé dans plusieurs fichiers !

On poursuivra la découverte de TypeScript dans un prochain article.

Laisser un commentaire