Cet article est une traduction dont l'original est Omit for discrimated unions par Dominik Dorfmeister TkDodo
Optimiser l'utilisation de Omit pour les Unions Discriminées en TypeScript
TypeScript propose des types utilitaires intégrés pour faciliter la transformation des types d'objet, comme Omit et Pick. Lors de la création de composants React qui agissent comme des wrappers autour de primitives de bas niveau, Omit s'avère utile pour définir les Props, car il permet de lier votre implémentation à celle sous-jacente.
Exemple de SelectProps
type SelectProps = {
onChange: (value: string) => void
options: ReadonlyArray<SelectOption>
value: string | null
}
type UserSelectProps = Omit<SelectProps, 'options'>
Ici, nous disons que nous voulons toutes les props du composant dont nous dépendons, à l'exception d'un ou plusieurs éléments. Nous pouvons ensuite construire notre composant UserSelect en utilisant les props tout en définissant nous-mêmes les éléments manquants.
function UserSelect(props: UserSelectProps) {
const users = useSuspenseQuery(usersQueryOptions)
return <Select {...props} options={users.data} />
}
Cette approche présente deux avantages :
- Pas besoin de redéclarer (et donc de copier) tous les champs de
SelectProps lors de la création de composants wrapper. - Ils restent automatiquement synchronisés.
Types d'Unions Discriminées
Ajoutons une nouvelle fonctionnalité à Select : une prop clearable, qui permet aux utilisateurs de désélectionner la valeur actuelle. Si cela se produit, nous voulons déclencher onChange avec null. Un premier brouillon pour les types pourrait ressembler à ceci :
type SelectProps = {
onChange: (value: string | null) => void
options: ReadonlyArray<SelectOption>
value: string | null
clearable?: boolean
}
Cependant, cela introduit un nouveau problème : toutes les utilisations existantes de Select génèrent une erreur car leurs gestionnaires onChange ne gèrent pas null. Nous voulons signaler au vérificateur de types : "Si nous passons clearable, onChange pourrait recevoir null, sinon ce ne sera pas le cas".
Les Unions Discriminées peuvent nous aider avec cela :
type BaseSelectProps = {
options: ReadonlyArray<SelectOption>
value: string | null
}
type ClearableSelectProps = BaseSelectProps & {
clearable: true
onChange: (value: string | null) => void
}
type UnclearableSelectProps = BaseSelectProps & {
onChange: (value: string) => void
clearable?: false
}
type SelectProps = ClearableSelectProps | UnclearableSelectProps
Bien que cela semble plus compliqué qu'auparavant, c'est avantageux. Maintenant, TypeScript peut discriminer l'union sur le drapeau clearable. Si true est passé, onChange aura une structure différente que si false ou rien n'est passé.
Problème avec UserSelect
Notre nouvelle fonctionnalité clearable devient également compatible au niveau des types. Cependant, nous découvrons une erreur dans notre composant UserSelect :
Types of property 'clearable' are incompatible.
Type 'boolean | undefined' is not assignable to type 'false | undefined'.
Type 'true' is not assignable to type 'false'.
Cela peut sembler déroutant, car nous composons simplement des types avec Omit , et cela fonctionnait avant. En inspectant ce que UserSelectProps développe, nous constatons que l'union qui discrimine sur clearable a disparu.
type UserSelectProps = {
onChange:
| ((value: string | null) => void)
| ((value: string) => void)
value: string | null
clearable?: boolean | undefined
}
L'ajout de Omit a "étendu" tout, ce qui n'était pas l'effet souhaité.
Types Conditionnels Distributifs
Les types conditionnels permettent à TypeScript de choisir entre deux types en fonction d'un test. Pour créer un helper Omit qui fonctionne mieux avec nos unions, nous pouvons utiliser les Types Conditionnels Distributifs.
type DistributiveOmit<T, K extends keyof T> = T extends any
? Omit<T, K>
: never
Si nous appliquons cela à nos UserSelectProps :
type UserSelectProps = DistributiveOmit<SelectProps, 'options'>
Cela développe une union de types où Omit est appliqué à chaque partie de notre type d'union, et UserSelect bénéficiera désormais implicitement de la fonctionnalité clearable.
Conclusion
L'utilisation de DistributiveOmit nous permet d'avoir une approche plus robuste pour gérer les types d'union tout en omettant les clés spécifiques. Cela offre également un avantage supplémentaire en signalant les clés que nous tentons d'omettre et qui n'existent plus, nous aidant à maintenir un code propre et efficace.
N'hésitez pas à me contacter si vous avez des questions ou à laisser un commentaire ci-dessous.