Aller au contenu principal

TypeScript et les objets

Définition

Contrairement à JavaScript où la vérification des types se fait au moment de l'exécution, TypeScript permet de définir les types des propriétés d’un objet à l'avance, garantissant ainsi une meilleure sécurité de type et une vérification dès la compilation.

Il existe plusieurs manières de définir le type d’un objet en TypeScript, mais les deux méthodes principales sont :

  • Utiliser une interface.
  • Utiliser un type.

Utiliser type pour typer les objets (attributs et méthodes)

Définition

type Student = {
name: string;
age: number;
courses: string[];
enrolled: boolean;
};

Lorsque nous créons un objet de ce type, TypeScript nous force à respecter cette structure.

type Student = {
name: string;
age: number;
courses: string[];
enrolled: boolean;
};

const student: Student = {
name: 'Alice',
age: 21,
courses: ['Math', 'Physics'],
enrolled: true
};

console.log(student.name);//Alice
student.courses.forEach((course) => {
console.log(course); // Math Physics
});

Les méthodes dans les objets

En TypeScript, un objet peut aussi contenir des méthodes, c’est-à-dire des fonctions associées à cet objet. Dans ce cas, on peut également définir le type des paramètres d'une méthode ainsi que son type de retour.

Exemple d’objet avec des méthode

type Student = {
name: string;
age: number;
courses: string[];
enrolled: boolean;
printInfo1: () => void;
printInfo2: () => void;
printInfo3: () => void;
};

const student: Student = {
name: 'Alice',
age: 21,
courses: ['Math', 'Physics'],
enrolled: true,
//Ecriture à privilégier
printInfo1: function() {
console.log(`${this.name}, age: ${this.age}`);
},
//Ne compile pas
printInfo2: () =>{
console.log(`${this.name}, age: ${this.age}`);
},
//fonctionne, mais accède en dur à student.
//si on change le nom de l'instance il faut changer
//aussi le contenu de printInfo3
printInfo3: () =>{
console.log(`${student.name}, age: ${student.age}`);
}
};
ATTENTION à this
  • la méthode printInfo1 n'accepte aucun paramètre et ne retourne rien (void). Elle accède aux propriétés de l’objet via le mot-clé this.
  • la méthode printInfo2 est une fonction fléchée. Elle ne peut pas accèder aux propriétés de l’objet via le mot-clé this. La compilation est impossible.
  • la méthode printInfo3 est une fonction fléchée. Elle accède explicitement au nom de l'instance.

Attention donc à comment vous utilisez this et les fonction fléchées. En règle générale, this fait référence à l'objet courant dans lequel la fonction est appelée. Cependant, le comportement de this dépend du contexte d'exécution, c’est-à-dire la manière dont une fonction est appelée.

Problème courant : this dans une fonction callback

Nous avons déjà vu ce problème l'année dernière, mais un petit rappel semble important. Le thisdans une fonction callback est un peu contre-intuitif.

Prenons un exemple avec une méthode qui parcourt un tableau et affiche les cours de l’étudiant :

const student: Student = {
name: 'Alice',
courses: ['Math', 'Physics'],
printCourses: function() {
this.courses.forEach(function(course) {
console.log(`${this.name} suit le cours ${course}`);
});
}
};

En apparence, cela semble correct, mais lorsqu’on compile cette fonction, deux erreurs remontent.

#erreur 1
this.courses.forEach(function(course) {
~~~~~~~~
An outer value of 'this' is shadowed by this container.

# erreur 2 (due à la précédente)
'this' implicitly has type 'any' because it does not have a type annotation.
console.log(`${this.name} suit le cours ${course}`);

Le problème vient du fait que this à l'intérieur de la fonction callback de forEach. Ce n’est pas le même this que dans la méthode printCourses. En effet, forEach est une méthode qui appelle sa fonction callback avec un contexte différent, et par conséquent this ne fait plus référence à l'objet student.

Solution : Utiliser une fonction fléchée

Pour résoudre ce problème, on peut utiliser une fonction fléchée. Nous l'avons déjà évoqué, les fonctions fléchées n’ont pas leur propre this, elles héritent de celui de leur contexte d’écriture (ici student), ce qui évite ce genre de bug. Ici, la fonction fléchée dans le forEach permet à this de faire correctement référence à l'objet student.

const student : Student = {
name: 'Alice',
courses: ['Math', 'Physics'],
printCourses: function() {
this.courses.forEach((course) => {
console.log(this.name + ' suit le cours ' + course);
});
}
};
En résumé :
  • printCourses ne doit pas être une fonction fléchée car on souhaite accéder au thisde l'instance
  • forEach doit utiliser une fonction fléchée sinon le this.name fait référence au contexte de function(course)

Record en TypeScript

Record est un type utilitaire de TypeScript. Il est utilisé pour créer des objets avec des clés de types spécifiques et des valeurs de types également spécifiques.

Record<K, T>
  • K : représente le type des clés de l'objet. Ce type peut être une union de valeurs littérales ou un type de base comme string ou number.
  • T : représente le type des valeurs de l'objet.

En d'autres termes, Record<K, T> signifie "un objet où les clés sont de type K et les valeurs de type T". Cela nous permet de créer un type d'objet plus structuré et d'éviter des erreurs liées aux types lors de la manipulation des objets.

Exemple 1 : Créer un dictionnaire avec des clés et des valeurs de types spécifiques

type Language = "fr" | "en" | "es";
type Greeting = "Bonjour" | "Hello" | "Hola";

// Utilisation de Record pour créer un dictionnaire avec des clés de type 'Language' et des valeurs de type 'Greeting'
const greetings: Record<Language, Greeting> = {
fr: "Bonjour",
en: "Hello",
es: "Hola",
};

Ici :

  • Les clés sont de type Language (soit "fr", "en", "es").
  • Les valeurs sont de type Greeting (soit "Bonjour", "Hello", "Hola").

L'utilisation de Record<Language, Greeting> permet de garantir qu'on n'ajoute que des paires clé-valeur valides, en s'assurant que chaque clé (comme "fr") correspond à une valeur appropriée (comme "Bonjour").

Exemple 2 : Créer un objet où les clés sont des index numériques et les valeurs des types personnalisés

type Product = { id: number; name: string; price: number };

// Un Record où les clés sont des identifiants de produits et les valeurs sont des objets de type Product
const productCatalog: Record<number, Product> = {
101: { id: 101, name: "Laptop", price: 999.99 },
102: { id: 102, name: "Smartphone", price: 499.99 },
103: { id: 103, name: "Keyboard", price: 79.99 },
};

Aller plus loin avec le TS

Dans le cdre de ce cours nous n'avons pas le temps d'approfondir le TS.

Les Interfaces :

interface est principalement utilisée pour définir des contrats d'objets et peut être étendue ou implémentée par d'autres interfaces ou classes (proche de ce que vous connaissez en java).

interface Student {
name: string;
age: number;
courses: string[];
}

interface StudentWithEnrollment extends Student {
enrolled: boolean;
}

const student1: StudentWithEnrollment = {
name: 'Alice',
age: 21,
courses: ['Math', 'Physics'],
enrolled: true
};
  • Ici, StudentWithEnrollment étend Student en ajoutant une nouvelle propriété enrolled.

  • interface est adaptée à la définition de types pour les classes. On peut implémenter une interface dans une classe comme en java alors que type ne peut pas être directement utilisé avec implements dans une classe.

Exemple avec interface et implements :

interface Person {
name: string;
greet(): void;
}

class Student implements Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}`);
}
}
En résumé

On utilise interface si on veut décrire des objets qui doivent être étendus ou implémentés dans des classes.

Autres fonctionalités liées au TS

TS fournit encore beaucoup de fonctioalités qu'on retrouve dans les langages fortement types (comme par exemple le java.)

Les énumérations

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

Les types génériques

function echo<T>(arg: T): T {
return arg;
}

Les décorateurs

function logClass(target: Function) {
console.log(`Class ${target.name} has been decorated`);
}

@logClass
class MyClass {
constructor() {
console.log("MyClass instance created");
}
}

Les classes abstraites

abstract class Animal {
abstract makeSound(): void;

move(): void {
console.log('Moving...');
}
}

class Dog extends Animal {
makeSound() {
console.log('Bark!');
}
}

const myDog = new Dog();
myDog.makeSound(); // Outputs: Bark!
myDog.move(); // Outputs: Moving...

Si vous souhaitez creuser la question vous pouvez consulter ce site.

Exercice

Voici un code JavaScript. Analysez le et transformez en au format ts.

const person = {
name: "Alice",
age: 30,
hobbies: ["reading", "swimming", "coding"],
sayHello() {
return `Hello, my name is ${this.name}`;
},
updateAge(newAge) {
this.age = newAge;
},
printHobbies() {
this.hobbies.map(function(hobby) {
console.log(`${this.name} likes ${hobby}`);
});
}
};

function greetPerson(personObj) {
console.log(personObj.sayHello());
personObj.updateAge(35);
console.log(`Age updated to: ${personObj.age}`);
personObj.printHobbies();
}

greetPerson(person);

Le code doit fonctionner sans erreurs, et produire la sortie suivante (ce n'est pas le cas actuellement avec la version JS) :

Hello, my name is Alice
Age updated to: 35
Alice likes reading
Alice likes swimming
Alice likes coding