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 static-typing
Le premier apport de TypeScript, ce qui lui vaut son nom, est le typage.
Variables
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
Fonctions
On peut indiquer le type du retour d’une fonction :
function helloWorld():string { return "Hello World"; }
Intérêt
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.
Concepts de l'Orienté-Objet
TypeScript complète JavaScript avec des éléments de la programmation orientée-objet.
Les interfaces
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’interfaceUser
, comme c’est le cas en PHP avec la clauseimplements
(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/
.
Les classes
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 commeprivate
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.
La modularité
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.
Export
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’interfaceVideo
, 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'
Import
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) ...