Débloquez la puissance de TypeScript avec des types conditionnels et mappés avancés. Créez des applications flexibles et de type sûr qui s’adaptent aux structures de données complexes.
Modèles TypeScript avancés : Maîtrise des types conditionnels et mappés
La puissance de TypeScript réside dans sa capacité à fournir un typage fort, vous permettant de détecter les erreurs tôt et d’écrire un code plus facile à maintenir. Bien que les types de base tels que string
, number
et boolean
soient fondamentaux, les fonctionnalités avancées de TypeScript, comme les types conditionnels et mappés, débloquent une nouvelle dimension de flexibilité et de sécurité des types. Ce guide complet se penchera sur ces concepts puissants, vous donnant les connaissances nécessaires pour créer des applications TypeScript véritablement dynamiques et adaptables.
Que sont les types conditionnels ?
Les types conditionnels vous permettent de définir des types qui dépendent d’une condition, comme un opérateur ternaire en JavaScript (condition ? trueValue : falseValue
). Ils vous permettent d’exprimer des relations de types complexes selon qu’un type satisfait ou non une contrainte spécifique.
Syntaxe
La syntaxe de base d’un type conditionnel est la suivante :
T extends U ? X : Y
T
 : Le type vérifié.U
 : Le type par rapport auquel effectuer la vérification.extends
 : Le mot clé indiquant une relation de sous-type.X
 : Le type à utiliser siT
est attribuable ĂU
.Y
 : Le type à utiliser siT
n’est pas attribuable ĂU
.
Essentiellement, si T extends U
prend la valeur true, le type est résolu en X
 ; sinon, il est résolu en Y
.
Exemples pratiques
1. Détermination du type d’un paramètre de fonction
Supposons que vous souhaitiez créer un type qui détermine si un paramètre de fonction est une chaîne ou un nombre :
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("La valeur est une chaîne :", value);
} else {
console.log("La valeur est un nombre :", value);
}
}
processValue("hello"); // Output: Value is a string: hello
processValue(123); // Output: Value is a number: 123
Dans cet exemple, ParamType<T>
est un type conditionnel. Si T
est une chaîne, le type est résolu en string
 ; sinon, il est résolu en number
. La fonction processValue
accepte une chaîne ou un nombre en fonction de ce type conditionnel.
2. Extraction du type de retour en fonction du type d’entrée
Imaginez un scénario dans lequel vous avez une fonction qui renvoie différents types en fonction de l’entrée. Les types conditionnels peuvent vous aider à définir le type de retour correct :
interface StringProcessor {
process(input: string): number;
}
interface NumberProcessor {
process(input: number): string;
}
type Processor<T> = T extends string ? StringProcessor : NumberProcessor;
function createProcessor<T extends string | number>(input: T): Processor<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processor<T>;
} else {
return { process: (input: number) => input.toString() } as Processor<T>;
}
}
const stringProcessor = createProcessor("example");
const numberProcessor = createProcessor(42);
console.log(stringProcessor.process("example")); // Output: 7
console.log(numberProcessor.process(42)); // Output: "42"
Ici, le type Processor<T>
sélectionne conditionnellement StringProcessor
ou NumberProcessor
en fonction du type d’entrée. Cela garantit que la fonction createProcessor
renvoie le type correct d’objet processeur.
3. Unions discriminées
Les types conditionnels sont extrêmement puissants lorsqu’ils sont utilisés avec des unions discriminées. Une union discriminée est un type d’union où chaque membre a une propriété de type singleton commune (le discriminant). Cela vous permet de réduire le type en fonction de la valeur de cette propriété.
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
type Area<T extends Shape> = T extends { kind: "square" } ? number : string;
function calculateArea(shape: Shape): Area<typeof shape> {
if (shape.kind === "square") {
return shape.size * shape.size;
} else {
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Output: 25
console.log(calculateArea(myCircle)); // Output: 28.274333882308138
Dans cet exemple, le type Shape
est une union discriminée. Le type Area<T>
utilise un type conditionnel pour déterminer si la forme est un carré ou un cercle, renvoyant un number
pour les carrés et une string
pour les cercles (bien que dans un scénario réel, vous souhaiteriez probablement des types de retour cohérents, cela démontre le principe).
Principaux points Ă retenir sur les types conditionnels
- Permettre de définir des types en fonction de conditions.
- Améliorer la sécurité des types en exprimant des relations de types complexes.
- Sont utiles pour travailler avec des paramètres de fonction, des types de retour et des unions discriminées.
Que sont les types mappés ?
Les types mappés offrent un moyen de transformer des types existants en mappant leurs propriétés. Ils vous permettent de créer de nouveaux types basés sur les propriétés d’un autre type, en appliquant des modifications telles que rendre les propriétés facultatives, en lecture seule ou en modifiant leurs types.
Syntaxe
La syntaxe générale d’un type mappé est la suivante :
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
 : Le type d’entrée.keyof T
 : Un opérateur de type qui renvoie une union de toutes les clés de propriétés dansT
.K in keyof T
 : Itère sur chaque clé danskeyof T
, en attribuant chaque clé à la variable de typeK
.ModifiedType
 : Le type auquel chaque propriété sera mappée. Cela peut inclure des types conditionnels ou d’autres transformations de types.
Exemples pratiques
1. Rendre les propriétés facultatives
Vous pouvez utiliser un type mappé pour rendre toutes les propriétés d’un type existant facultatives :
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Valid, as 'id' and 'email' are optional
Ici, PartialUser
est un type mappé qui itère sur les clés de l’interface User
. Pour chaque clé K
, il rend la propriété facultative en ajoutant le modificateur ?
. Le User[K]
récupère le type de la propriété K
à partir de l’interface User
.
2. Rendre les propriétés en lecture seule
De même, vous pouvez rendre toutes les propriétés d’un type existant en lecture seule :
interface Product {
id: number;
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
const readonlyProduct: ReadonlyProduct = {
id: 123,
name: "Example Product",
price: 25.00,
};
// readonlyProduct.price = 30.00; // Error: Cannot assign to 'price' because it is a read-only property.
Dans ce cas, ReadonlyProduct
est un type mappé qui ajoute le modificateur readonly
à chaque propriété de l’interface Product
.
3. Transformation des types de propriétés
Les types mappés peuvent également être utilisés pour transformer les types de propriétés. Par exemple, vous pouvez créer un type qui convertit toutes les propriétés de chaîne en nombres :
interface Config {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type NumericConfig = {
[K in keyof Config]: Config[K] extends string ? number : Config[K];
};
const numericConfig: NumericConfig = {
apiUrl: 123, // Must be a number due to the mapping
timeout: 456, // Must be a number due to the mapping
maxRetries: 3,
};
Cet exemple montre comment utiliser un type conditionnel dans un type mappé. Pour chaque propriété K
, il vérifie si le type de Config[K]
est une chaîne. Si c’est le cas, le type est mappé à number
 ; sinon, il reste inchangé.
4. Remappage des clés (depuis TypeScript 4.1)
TypeScript 4.1 a introduit la possibilité de remapper les clés dans les types mappés à l’aide du mot clé as
. Cela vous permet de créer de nouveaux types avec des noms de propriétés différents basés sur le type d’origine.
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Result:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize function used to Capitalize first letter
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Usage with an actual object
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
Ici, le type TransformedEvent
remappe chaque clé K
vers une nouvelle clé préfixée par « new » et mise en majuscule. La fonction d’utilitaire `Capitalize` garantit que la première lettre de la clé est mise en majuscule. L’intersection `string & K` garantit que nous ne traitons que des clés de chaîne et que nous obtenons le type littéral correct de K.
Le remappage des clés ouvre de puissantes possibilités de transformation et d’adaptation des types à des besoins spécifiques. Cela vous permet de renommer, de filtrer ou de modifier les clés en fonction d’une logique complexe.
Principaux points à retenir sur les types mappés
- Permettre de transformer des types existants en mappant leurs propriétés.
- Permettre de rendre les propriétés facultatives, en lecture seule ou de modifier leurs types.
- Sont utiles pour créer de nouveaux types basés sur les propriétés d’un autre type.
- Le remappage des clés (introduit dans TypeScript 4.1) offre une flexibilité encore plus grande dans les transformations de types.
Combinaison de types conditionnels et mappés
La véritable puissance des types conditionnels et mappés réside dans leur combinaison. Cela vous permet de créer des définitions de types très flexibles et expressives qui peuvent s’adapter à un large éventail de scénarios.Exemple : Filtrage des propriétés par type
Supposons que vous souhaitiez créer un type qui filtre les propriétés d’un objet en fonction de leur type. Par exemple, vous pouvez extraire uniquement les propriétés de chaîne d’un objet.
interface Data {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringData = StringProperties<Data>;
// Result:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
Dans cet exemple, le type StringProperties<T>
utilise un type mappé avec le remappage des clés et un type conditionnel. Pour chaque propriété K
, il vérifie si le type de T[K]
est une chaîne. Si c’est le cas, la clé est conservée ; sinon, elle est mappée à never
, ce qui la filtre efficacement. never
en tant que clé de type mappé la supprime du type résultant. Cela garantit que seules les propriétés de chaîne sont incluses dans le type StringData
.
Types utilitaires dans TypeScript
TypeScript fournit plusieurs types utilitaires intégrés qui exploitent les types conditionnels et mappés pour effectuer des transformations de types courantes. La compréhension de ces types utilitaires peut simplifier considérablement votre code et améliorer la sécurité des types.
Types utilitaires courants
Partial<T>
 : Rend toutes les propriétés deT
facultatives.Readonly<T>
 : Rend toutes les propriétés deT
en lecture seule.Required<T>
 : Rend toutes les propriétés deT
obligatoires. (supprime le modificateur?
)Pick<T, K extends keyof T>
 : Sélectionne un ensemble de propriétésK
Ă partir deT
.Omit<T, K extends keyof T>
 : Supprime un ensemble de propriétésK
Ă partir deT
.Record<K extends keyof any, T>
 : Construit un type avec un ensemble de propriétésK
de typeT
.Exclude<T, U>
 : Exclut deT
tous les types qui sont attribuables ĂU
.Extract<T, U>
 : Extrait deT
tous les types qui sont attribuables ĂU
.NonNullable<T>
 : Exclutnull
etundefined
deT
.Parameters<T>
 : Obtient les paramètres d’un type de fonctionT
dans un tuple.ReturnType<T>
 : Obtient le type de retour d’un type de fonctionT
.InstanceType<T>
 : Obtient le type d’instance d’un type de fonction de constructeurT
.ThisType<T>
 : Sert de marqueur pour le typethis
contextuel.
Ces types utilitaires sont construits à l’aide de types conditionnels et mappés, ce qui démontre la puissance et la flexibilité de ces fonctionnalités TypeScript avancées. Par exemple, Partial<T>
est défini comme suit :
type Partial<T> = {
[P in keyof T]?: T[P];
};
Meilleures pratiques pour l’utilisation des types conditionnels et mappés
Bien que les types conditionnels et mappés soient puissants, ils peuvent également rendre votre code plus complexe s’ils ne sont pas utilisés avec soin. Voici quelques bonnes pratiques à garder à l’esprit :
- Faites simple : Évitez les types conditionnels et mappés trop complexes. Si une définition de type devient trop alambiquée, envisagez de la décomposer en parties plus petites et plus faciles à gérer.
- Utilisez des noms explicites : Donnez à vos types conditionnels et mappés des noms descriptifs qui indiquent clairement leur objectif.
- Documentez vos types : Ajoutez des commentaires pour expliquer la logique derrière vos types conditionnels et mappés, surtout s’ils sont complexes.
- Tirez parti des types utilitaires : Avant de créer un type conditionnel ou mappé personnalisé, vérifiez si un type utilitaire intégré peut obtenir le même résultat.
- Testez vos types : Assurez-vous que vos types conditionnels et mappés se comportent comme prévu en écrivant des tests unitaires qui couvrent différents scénarios.
- Tenez compte des performances : Les calculs de types complexes peuvent avoir un impact sur les temps de compilation. Soyez attentif aux implications sur les performances de vos définitions de types.
Conclusion
Les types conditionnels et mappés sont des outils essentiels pour maîtriser TypeScript. Ils vous permettent de créer des applications très flexibles, de type sûr et faciles à maintenir qui s’adaptent aux structures de données complexes et aux exigences dynamiques. En comprenant et en appliquant les concepts abordés dans ce guide, vous pouvez débloquer tout le potentiel de TypeScript et écrire un code plus robuste et plus évolutif. Au fur et à mesure que vous continuez à explorer TypeScript, n’oubliez pas d’expérimenter différentes combinaisons de types conditionnels et mappés pour découvrir de nouvelles façons de résoudre des problèmes de typage difficiles. Les possibilités sont vraiment infinies.