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}`);
}
};
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 this
dans 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);
});
}
};
printCourses
ne doit pas être une fonction fléchée car on souhaite accéder authis
de l'instanceforEach
doit utiliser une fonction fléchée sinon lethis.name
fait référence au contexte defunction(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 commestring
ounumber
.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
étendStudent
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 quetype
ne peut pas être directement utilisé avecimplements
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}`);
}
}
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