Au chapitre précédent, j’ai fait un “Hello World” Typescript en
dessinant un rectangle dans un <canvas> HTML. En
chemin, j’ai laissé quelques questions en suspend. On va donc chercher à
y répondre aujourd’hui… et voir quelles autres questions se soulèvent au
passage.
Reprenons ce que je voulais faire:
- Refactoriser pour rajouter une classe
DrawingBoardqui sera responsable de maintenir une liste des objets à dessiner (qui utilisera donc l’interfaceDrawable) et une référence au contexte du<canvas>, et qui pourra dessiner d’un coup tous les objets. - Comprendre comment fonctionne exactement le
!, leas, et comment faire une gestion propre des exceptions pour ne pas juste reporter le problème au runtime. - Comment introduire correctement des tests dans tout ça.
Un point supplémentaire auquel j’ai pensé depuis est que j’aimerais bien commencer aussi à organiser mon code en différents fichiers, ce qui me permettra de chercher à comprendre comment fonctionnent les imports en Typescript.
Refactorisation
La première partie ne nous apporte pas grand chose de nouveau, si ce
n’est une petite séance de rappel sur comment fonctionne le
forEach et les fonctions anonymes en Javascript.
- Modification de l’interface pour que
drawprenne un contexte en argument (pour éviter que le rectangle doive s’en préoccuper lui-même):
interface Drawable {
draw(ctx: CanvasRenderingContext2D): void;
}- Modification en conséquence de la classe
Rectangle:
class Rectangle {
constructor(private x: number,
private y: number,
private w: number,
private h: number
){}
public draw(ctx: CanvasRenderingContext2D): void {
ctx.rect(this.x, this.y, this.w, this.h);
ctx.stroke();
}
}- Création de la classe
DrawingBoard, qui va contenir une liste deDrawable, garder le contexte, et dessiner chaque élément dans le contexte:
class DrawingBoard {
private drawings: Drawable[];
constructor(private ctx: CanvasRenderingContext2D) {
this.drawings = [];
}
public add(drawing: Drawable){
this.drawings.push(drawing);
}
public draw(): void {
this.drawings.forEach((e) => {
e.draw(this.ctx);
})
}
}- Et finalement: pour exécuter tout ça, on crée maintenant un
DrawingBoarddans lequel on ajoute le rectangle. En bonus, on peut facilement en faire un deuxième et vérifier que tout va bien:
const canvas: HTMLCanvasElement = document.getElementById("drawing") as HTMLCanvasElement;
const board = new DrawingBoard(canvas.getContext("2d")!);
board.add(new Rectangle(100, 150, 200, 50));
board.add(new Rectangle(150, 160, 15, 70));
board.draw();tsc drawing.ts pour compiler, et puis on peut tester
dans drawing.html,
dans lequel on n’a rien changé.

!?
Le Javascript a pas mal changé depuis la dernière fois que j’en ai fait “sérieusement”. Et encore, je n’ai jamais vraiment plongé “à fond” dans le langage. Une des difficultés ici en me lançant dans cette aventure Typescript, c’est de bien distinguer ce que je dois comprendre dans les particularités de la surcouche Typescript, et ce qui est juste une propriété de base du Javascript. Sinon, je passe beaucoup de temps à chercher dans le documentation des choses qui ne s’y trouvent pas.
L’utilisation des ! et des ? semble faire
partie des choses où il est parfois un peu compliqué de démêler ce qui
vient d’où. Essayons.
- Le
!, en Javascript, intervient à la base uniquement comme une négation, comme en Python:!=pour l’inégalité (ou!==pour l’inégalité stricte, puisque Javascript aime jouer avec du flou sur ce qui est égal ou vrai), et!<expression>pour l’opérateur de négation. - En Typescript, il peut aussi servir en “postfix”
<expression>!en tant qu’opérateur de “non-nullité”, qui ne change pas le fonctionnement du programme mais déclare que le type de ce qui précède ne sera ninull, niundefined.
Ce dernier usage du ! fait partie plus généralement des
“types
assertions”, comme le as qu’on utilise aussi dans notre
programme. Ce as n’est pas un “casting” en tant que tel,
mais bien une déclaration au compilateur que vous êtes tout à fait
confiant dans le fait que votre variable aura ce type là. On peut faire
cette déclaration soit dans le sens “plus spécifique”, comme on l’a fait
(HTMLElement vers HTMLCanvasElement), soit
dans l’autre sens. Je ne vois pas trop l’intérêt de cette dernière
fonctionnalité, d’ailleurs. Elle permet d’ailleurs en pratique, comme le
note la documentation, de faire toute déclaration que l’on veut en
combinant deux as avec le type any au milieu.
Par exemple:
const a = 1;
const b: string = a as any as string;
console.log(b);Va être parfaitement accepté par le compilateur, et va bien afficher
1 dans la console. Mais bon, si c’est pour faire ça, on
peut juste faire du Javascript directement…
Le ? mène aussi une double vie entre le Javascript et le
Typescript:
- En Javascript,
??est le “Nullish coalescing operator”, qui permet de donner une valeur par défaut pour remplacer unnullou unundefined.const a = null ?? 1assignera par exemple1àa. On a aussi son cousin le “Nullish coalescing assignment”, qui réalise une assignation uniquement si l’élément de gauche estnullouundefined:const a = null; a ??= 1;donnera mettra1dansa. - Toujours en Javascript mais dans un sens différent, on a le “chainage
optionnel” avec
?.. Si on veut par exemple fairea.prop.sousprop, mais qu’il est possible quea.propsoitundefinedounull, on peut écrirea.prop?.sousprop. Sipropestnullouundefined, Javascript s’arrête aprèsa.prop?et renvoie directementundefinedpour toute l’expression, sans renvoyer d’erreur. - En Typescript,
?peut aussi être utilisé en “postfix” pour indiquer l’optionnalité d’un paramètre:function f(x?: number)indique que le paramètrexpeut êtrenumberouundefined.
Reste donc la question de si c’est une bonne idée dans mon programme de faire:
const canvas: HTMLCanvasElement = document.getElementById("drawing") as HTMLCanvasElement;
const board = new DrawingBoard(canvas.getContext("2d")!);Qu’est-ce qui peut mal tourner ici ?
- Si
document.getElementById("drawing")n’existe pas. - Si
document.getElementById("drawing")n’est pas un<canvas>, mais un autre élément HTML. - Si
canvas.getContext("2d")renvoienull.
getContext
ne renverra null que si le “context identifier” (ici
"2d") n’est pas supporté, ce qui est fort peu probable
("2d" est la valeur par défaut), ou si le
<canvas> a déjà été initialisé avec un autre
contexte. Je ne vois pas vraiment de risque que ça arrive, donc je vais
pour l’instant le laisser comme ça. La syntaxe de la gestion des
exceptions me semble suffisamment normale pour que je ne m’en préoccupe
pas tant que je n’en ai pas vraiment besoin.
Par contre, si je fais des modifications dans le HTML, je pourrais
tout à fait me retrouver à un moment à changer l’identifiant du
canvas sans m’en rendre compte. Si je le fais – ça se teste
facilement – mes rectangle disparaissent, et j’ai dans la console une
erreur: “Uncaught TypeError: can’t access property”getContext”, canvas
is null”. Une option serait, dans ce cas, de créer directement le
canvas depuis notre code.
function createCanvas(): HTMLCanvasElement {
const canvas = <HTMLCanvasElement>document.createElement("canvas");
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
return canvas;
}
const canvas: HTMLCanvasElement = document.getElementById("drawing") as HTMLCanvasElement ?? createCanvas();
const board = new DrawingBoard(canvas.getContext("2d")!);
board.add(new Rectangle(100, 150, 200, 50));
board.add(new Rectangle(150, 160, 15, 70));
board.draw();Même si c’est sympa comme démonstration des fonctionnalités de
?? et as, c’est du coup un peu se compliquer
la vie. Ce n’est par contre sans doute pas une mauvaise idée de retirer
le <canvas> du HTML et de laisser la responsabilité
au fichier Javascript de le créer, sans avoir plus besoin de
?? et de as:
function createCanvas(w: number = 500, h: number = 500): HTMLCanvasElement {
const canvas = <HTMLCanvasElement>document.createElement("canvas");
canvas.width = w;
canvas.height = h;
document.body.appendChild(canvas);
return canvas;
}
const canvas = createCanvas();
const board = new DrawingBoard(canvas.getContext("2d")!);
board.add(new Rectangle(100, 150, 200, 50));
board.add(new Rectangle(150, 160, 15, 70));
board.draw();Et le <body> du fichier HTML devient minimal:
<body>
<script src="drawing.js"></script>
</body>Le résultat du programme est identique. Je ne sais pas trop si je
préfère cette version, où celle avec le <canvas> dans
le HTML. Je peux voir une certaine logique aux deux. Je vais garder
cette version-ci pour l’instant, mais je changerai peut-être d’avis plus
tard, une fois que j’aurai décidé un peu plus clairement quel “projet”
je développerai pour continuer à apprendre !
En attendant, assez pour l’instant.
Code:
À suivre…
Ce qu’on a laissé de côté:
- Comment introduire correctement des tests dans tout ça.
- Organiser le code en différents fichiers, ce qui me permettra de chercher à comprendre comment fonctionnent les imports.