Pourquoi types avances en TypeScript ?
Dans un monde où les projets deviennent de plus en plus complexes, l'utilisation de TypeScript permet aux développeurs d'écrire du code plus robuste et maintenable. Les types avancés offrent des fonctionnalités qui aident à prévenir les bugs en amont, à augmenter la lisibilité du code et à assurer la cohérence des structures de données. Un cas concret est l'utilisation de génériques pour créer des composants réutilisables et types sécurisés, ou encore les interfaces étendues qui permettent d'ajouter de nouvelles propriétés à des objets existants.
Prerequis
- Connaissance approfondie de TypeScript (pratiquement tout ce que vous avez appris jusqu'à présent)
- Compréhension des concepts de base : types primitifs, objets, tableaux, fonctions
- Familiarité avec les structures de contrôle avancées : boucles
for...of,map,filter - Connaissance des interfaces et des classes en TypeScript
Outils à installer :
- Node.js v14 ou supérieur
- npm (Node Package Manager)
- Visual Studio Code (ou n'importe quel éditeur de code moderne avec une bonne prise en charge de TypeScript)
Concepts fondamentaux
1. Génériques
Les génériques permettent aux fonctions et aux classes de travailler avec des types variables plutôt que des types fixes.
// ❌ Sans génériques
function identity(arg: any): any {
return arg;
}
let output = identity("myString");
// ✅ Avec génériques
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString");
2. Interfaces étendues
Les interfaces étendues permettent de combiner les définitions d'interfaces existantes.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const myDog: Dog = {
name: "Buddy",
breed: "Golden Retriever"
};
3. Types unions et intersections
Les unions et les intersections permettent de combiner des types en fonction de vos besoins.
// Unions : Permet de spécifier plusieurs types possibles
type Status = "loading" | "success" | "error";
function logStatus(status: Status) {
if (status === "loading") {
console.log("Loading...");
} else if (status === "success") {
console.log("Success!");
} else {
console.error("Error occurred");
}
}
// Intersections : Permet de combiner plusieurs types en un
interface A { a: string; }
interface B { b: number; }
type AB = A & B;
const obj: AB = { a: "Hello", b: 42 };
4. Types personnalisés avec type et interface
Bien que les deux soient utilisables pour définir des types, il est généralement recommandé d'utiliser interface pour les définitions complexes de structures de données et de type pour les alias de types simples.
// Interface
interface Point {
x: number;
y: number;
}
// Type
type Point = {
x: number;
y: number;
};
5. Types littéraux
Les types littéraux permettent de définir des types qui sont exactement les mêmes que les valeurs littérales.
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction) {
switch (direction) {
case "up":
console.log("Moving up");
break;
// autres cas...
}
}
Mise en pratique : projet fil rouge
Étape 1 : Création du projet
mkdir advanced-types-project
cd advanced-types-project
npm init -y
npm install typescript ts-node --save-dev
npx tsc --init
Structure de base du projet :
advanced-types-project/
├── src/
│ ├── index.ts
│ └── utils/
│ └── logger.ts
├── package.json
└── tsconfig.json
Étape 2 : Création des fichiers
src/utils/logger.ts
export interface LoggerOptions {
level: "info" | "warn" | "error";
}
export class Logger {
private options: LoggerOptions;
constructor(options: LoggerOptions) {
this.options = options;
}
log(message: string, level: LoggerOptions["level"]): void {
if (this.shouldLog(level)) {
console.log(`[${level.toUpperCase()}] ${message}`);
}
}
private shouldLog(level: LoggerOptions["level"]): boolean {
return this.options.level === "info" || this.options.level === level;
}
}
src/index.ts
import { Logger, LoggerOptions } from "./utils/logger";
const logger = new Logger({ level: "info" });
logger.log("This is an info message", "info");
logger.log("This is a warning message", "warn");
logger.log("This is an error message", "error");
Étape 3 : Configuration de TypeScript
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
Étape 4 : Compilation et exécution
npx tsc
node dist/index.js
Erreurs frequentes et debugging
1. Type d'argument incorrect
Code incorrect :
function add(a: number, b: string): number {
return a + b; // Erreur ici
}
const result = add(5, "10");
Code correct :
function add(a: number, b: number): number {
return a + b;
}
const result = add(5, 10);
2. Propriété non définie sur un objet
Code incorrect :
interface Person {
name: string;
}
function greet(person: Person) {
console.log(`Hello, ${person.age}!`);
}
const john = { name: "John" };
greet(john); // Erreur ici
Code correct :
interface Person {
name: string;
age?: number; // Optionnel
}
function greet(person: Person) {
console.log(`Hello, ${person.age ? person.age : 18}!`);
}
const john = { name: "John" };
greet(john);
3. Accès à une propriété inexistante sur un type
Code incorrect :
interface User {
id: number;
}
function getUserInfo(user: User) {
return user.email; // Erreur ici
}
Code correct :
interface User {
id: number;
email?: string; // Optionnel
}
function getUserInfo(user: User): string | undefined {
return user.email;
}
Pour aller plus loin
1. Types de tuples
Les types de tuples permettent d'exprimer un tableau avec des éléments de différents types fixés.
type Point = [number, number];
const point: Point = [10, 20];
2. Types génériques avec contraintes
Les types génériques peuvent avoir des contraintes pour s'assurer que les types utilisés respectent certaines conditions.
function logLength<T extends { length: number }>(arg: T): void {
console.log(`Length is ${arg.length}`);
}
logLength([1, 2, 3]);
logLength("Hello");
3. Types de fonction avec des types de retour
Les fonctions peuvent avoir des types de retour spécifiques.
function add(a: number, b: number): number {
return a + b;
}
const result = add(5, 10);
Défi pratique : Créer un type générique pour une fonction qui prend un tableau et renvoie le dernier élément
function getLastItem<T>(array: T[]): T | undefined {
if (array.length === 0) {
return undefined;
}
return array[array.length - 1];
}
const numbers = [1, 2, 3, 4, 5];
const lastNumber = getLastItem(numbers);
console.log(lastNumber); // Output: 5
Ce défi vous permettra de mettre en pratique les concepts de génériques et d'intersections que nous avons vus dans ce tutoriel.