txs:contrib:peertube_a18:concepts_typescript

Concepts centraux de TypeScript

PeerTube est développé à l’aide de TypeScript, un langage apportant des fonctionnalités supplémentaires à JavaScript. Conçu par Anders Hejlsberg (concepteur de C#, chez Microsoft), le but du langage est de permettre de développer des applications à plus grande échelle et d’augmenter la fiabilité et la maintenabilité du code.

TypeScript offre des possibilités qui reposent sur les concepts centraux suivants.

Le premier apport de TypeScript, ce qui lui vaut son nom, est le typage.

Il est possible d’associer un type à une donnée. On retrouve trois types : number, string et boolean, respectivement pour les nombres entiers et flottants, les chaînes de caractères et les booléens.

Lors de l’initialisation d’une variable, TypeScript infère automatiquement son type s’il n’a pas été déclaré. Lorsqu’il ne parvient pas à le déterminer il utilise le type any.

Une déclaration de variables se fait comme suit :

var page: number = 7; // annotation de type
var completeVideoDescription: string;
var remoteServerDown = true; // boolean (implicite)
var joker: any;
var joker2; // any

On peut indiquer le type du retour d’une fonction :

function helloWorld():string {
    return "Hello World";
}

Cette possibilité de déclarer explicitement le type d’une variable est nommée static-typing.

Mais l’intérêt, c’est qu’on donne plus d’informations sur notre programme, qui vont permettre au type-checker de TypeScript de signaler les erreurs ou oublis. Sur ce point, JavaScript était permissif, ce qui pouvait occasionner des erreurs qu’on met du temps à débugger.

TypeScript complète JavaScript avec des éléments de la programmation orientée-objet.

Supposons qu’on ait un objet user, défini par son firstName et lastName. On veut que tous les users possèdent toujours ces deux propriétés. Les interfaces, en définissant la forme que doivent avoir les objets, permettent de s’en assurer :

interface User {
    firstName: string;
    lastName: string;
}
 
function greeter(user: User):string {
    return "Hello, " + user.firstName + " " + user.lastName;
}
 
let user = { firstName: "John", lastName: "Doe", age: 20 };
 
document.body.innerHTML = greeter(user);

Le type-checker de TypeScript va vérifier que l’objet user passé à la fonctiongreeter correspond bien à l’interface User. Deux choses à remarquer :

  • on peut ne pas déclarer explicitement que user implémente l’interface User, comme c’est le cas en PHP avec la clause implements (on verra avec les classes un exemple d’utilisation de cette clause);
  • user possède en plus une propriété age mais le type-checker vérifie juste que les propriétés présentes dans l’interface sont bien là.

On trouvera souvent ces interfaces déclarées en haut des fichiers de code.

A la différence d’une structure en C, une interface n’est pas instanciable, elle permet le contrôle de la structuration d’une variable ou d’une instance de classe. Cela fonctionne en revanche comme en Java, où l’on instancie des classes, mais pas des interfaces.

Une interface n’est pas une classe. Ce que fait l’interface, c’est décrire la forme des objets. Ce que fait la classe, c’est implémenter la création des objets.


Dans le code de PeerTube, les interface sont rangées dans le dossier shared/models/videos/.

Déclaration

Les classes en TypeScript se déclarent comme dans l’exemple suivant :

class VideoWatchComponent { 
  //// attributs 
  player: videojs.Player
  completeDescriptionShown = false
  completeVideoDescription: string
  shortVideoDescription: string
  videoHTMLDescription = '' 
  ...
 
  //// constructeur 
  constructor(
    constr_videoHTMLDescription: string,
    private elementRef: ElementRef,
    private changeDetector: ChangeDetectorRef,
    private route: ActivatedRoute,
    private router: Router,
    ...
    ) { 
      this.videoHTMLDescription = constr_videoHTMLDescription; 
   }  
 
  //// méthodes
  // un getter :
  get user () {
    return this.authService.getUser()
  }
 
  // un setteur :
  setLike () {
    if (this.isUserLoggedIn() === false) return
    if (this.userRating === 'like') {
      // Already liked this video
      this.setRating('none')
    } else {
      this.setRating('like')
    }
  }
 
  // une autre fonction de la classe :
  showMoreDescription () {
    if (this.completeVideoDescription === undefined) {
      return this.loadCompleteDescription()
    }
 
    this.updateVideoDescription(this.completeVideoDescription)
    this.completeDescriptionShown = true
  }
}

Cet exemple est basé sur la classe déclarée dans le fichier client/src/app/videos/+video-watch/video-watch.component.ts.

Une déclaration de classe se structure donc par des attributs (variables définies sans le mot clé var), un constructeur et des méthodes (fonctions définies sans le mot clé function).

Attributs et méthodes peuvent être de 4 types différents :

  • public : c’est le type implicite, il signifie que n’importe qui peut accéder à l’élément en question ;
  • private : non implicite, il rend un élément non accessible depuis l’extérieur ou même par une sous-classe ;
  • protected : fonctionne comme private mais permet aux sous-classes d’accéder à l’élément ;
  • readonly : l’élément est défini une fois pour toutes au moment de la déclaration de la classe et n’est plus modifiable par la suite.

Instanciation

Pour instancier la classe déclarée précédemment, on peut procéder comme suit :

var ma_classe = new VideoWatchComponent("Description de la vidéo...");

Cette instanciation permet grâce au constructeur de d’initaliser l’attribut videoHTMLDescription. Pour y accéder, on pourrait utiliser deux méthodes :

  • L’accès direct à l’attribut en question, s’il n’est pas private :
ma_classe.videoHTMLDescription;
  • L’accès par une méthode getter (dans l’optique où il a été déclaré) :
ma_classe.getVideoHTMLDescription();

Héritage

De pair avec la notion de classe, TypeScript implémente la notion d’héritage simple par l’utilisation du mot-clé extends.

Dans le fichier /client/src/app/videos/video-list/video-trending.component.ts, la classe VideoTrendingComponent hérite de la classe abstraite AbstractVideoList

class VideoTrendingComponent extends AbstractVideoList { 
  titlePage: string
 
  ...
 
  getVideosObservable (page: number) {
    const newPagination = immutableAssign(this.pagination, { currentPage: page })
    return this.videoService.getVideos(newPagination, this.sort, undefined, this.categoryOneOf)
  }
 
  ...
 
}

Cette nouvelle classe VideoTrendingComponent ajoute un nouvel attribut titlePage à la classe AbstractVideoList et redéfinit la méthode getVideosObservable.

TypeScript permet un pseudo-héritage multiple en combinant classes et interfaces. Grâce au mot-clé implements, une classe peut “hériter” de la structure de deux interfaces. C’est le cas pour VideoWatchComponent dans le code de PeerTube, qui hérite de OnInit et OnDestroy :

class VideoWatchComponent implements OnInit, OnDestroy {
  ...
}

Comme dans d’autres langages orientés-objet, une classe peut être abstraite. On utilise le mot-clé abstract.

Enfin, comme en JavaScript, le mot-clé super permet à une classe-fille de référer à un élément d’une classe-mère.

TypeScript introduit différentes techniques permettant de concevoir des applications modulaires et de grandes tailles, à l’instar de Peertube. Le concept de module y est central.

Lorsque l’on déclare une variable, une classe, une fonction, etc. dans un fichier, celle-ci n’est visible et utilisable par un autre fichier qu’à la condition d’utiliser la mention export. Inversement, pour utiliser un objet déclaré dans un autre fichier (ou module), il est nécessaire de faire son import dans le module courant.

Il existe deux façons, combinables, d’exporter un élément d’un module :

  • La mention export au début de sa déclaration ; comme pour l’interface Video, déclarée dans le fichier ./shared/models/videos/video.model.ts :
<code javascript>

export interface Video {

id: number
uuid: string
createdAt: Date | string
updatedAt: Date | string
publishedAt: Date | string
...

} </code>

  • En rajoutant une ligne à cet effet après la déclaration de l’élément à exporter. On pourrait alors réécrire l’exemple précédent :
<code javascript>

/* export → non nécessaire */ interface Video {

...

}

export { Video }; ou export { Video as interfVideo }; pour renommer l’interface lorsqu’utilisée hors de ce module </code>

Ces deux techniques sont utilisées dans le code de PeerTube de façon redondante. Le mot-clé export précède les déclarations dans les modules concernés et dans chaque sous-dossier de ./shared un fichier index.ts semble gérer l’accès des modèles aux branches supérieures de l’arborescence. Ainsi, ./shared/models/videos/index.ts contient les exports de tous les fichiers apparentés :

export * from './rate/user-video-rate-update.model' // * signifie qu'on exporte TOUT ce que contient le module
export * from './rate/user-video-rate.model'
export * from './rate/user-video-rate.type'
...
export * from './import/video-import-create.model'
export * from './import/video-import-state.enum'
export * from './import/video-import.model'
export { VideoConstant } from './video-constant.model'

L’importation est encore plus simple que l’exportation. On peut importer un seul élément d’un module, en rajoutant dans l’en-tête :

import { VideoModel } from './video'

On peut, de même qu’avec export, renommer un élément importé :

import { ScopeNames as VideoScopeNames, VideoModel } from './video'

Il est également possible d’importer l’intégralité d’un module dans une variable, par laquelle on accède aux éléments dudit module :

import * as Sequelize from 'sequelize'
 
...
const escapedSearch = VideoModel.sequelize.escape(options.search)
...
  • txs/contrib/peertube_a18/concepts_typescript.txt
  • de 127.0.0.1