Exploiter les Discriminated Union en Typescript

19358 단어

De quoi parle-t-on ?



En Typescript, il est possible de définir des types de plusieurs manières: interface, classe, enum, mot-clé type , as const , 등 Dans cet 기사, nous allons nous concentrer sur les types construits à partir d'une union disjointe et les avantages d'une telle pratique. L'union en Typescript se fait via le symbole | (예: type Union = A | B | C ). Le terme disjoint n'est pas anodin car contrairement au polymorphisme, les types que l'on va utiliser peuvent ne rien avoir en commun.

미장 상황



Prenons un exemple très simple, la représentation des utilisateurs dans une application. Ces utilisateurs peuvent être des invités(손님), des 클라이언트(고객) ou des 관리자(관리자). Les utilisateurs connectés ont un identifiant et les administrators ont des permission spécifiques 친척 à leur domaine d'administration. Contruisons donc une 인터페이스 User 실용주의자를 따르십시오.

interface User {
  userType: "Guest" | "Customer" | "Admin";
  login?: string;
  accessRights?: string[]; // could be more specific
}


특정 소유주는 옵션이 없습니다. car elles n'existent pas pour l'utilisateur invité(et qu'on use le mode strict du compilateur Typescript).

Cette 인터페이스 nous permet de créer des utilisateurs valides:

const johnDoe: User = {
  userType: "Admin",
  login: "JohnDoe",
  accessRights: ["database", "monitoring"],
};
const guest: User = {
  userType: "Guest",
};


Mais nous pouvons également créer des utilisateurs qui 특파원 à des cas non souhaités :

const customerWithoutLogin: User = {
  userType: "Customer",
};
const guestWithAccessRights: User = {
  userType: "Guest",
  accessRights: ["fs"],
};


Et on ne peut pas garantir si par exemple l'identifiant existe ou pas:

johnDoe.login.toUpperCase(); // Object is possibly 'undefined'.
guest.login.toUpperCase(); // Object is possibly 'undefined'.
customerWithoutLogin.login.toUpperCase(); // Object is possibly 'undefined'.
guestWithAccessRights.login.toUpperCase(); // Object is possibly 'undefined'.


En général on finit par utiliser une condition ou du chainage optionnel, ce qui renforce l'incertitude sur le fonctionnement au runtime :

// Which one is really executed ?
johnDoe.login?.toUpperCase();
guest.login?.toUpperCase();
customerWithoutLogin.login?.toUpperCase();
guestWithAccessRights.login?.toUpperCase();


Pour réduire cette incertitude, il ne nous reste plus qu'à écrire des tests unitaires, faire du monitoring, du debug et lever des exceptions. Heureusement, nous pouvons éviter tout ca avec un meilleur type.

차별된 조합



Explicit is better than implicit



Nous avons trois 유형 d'utilisateurs distinct et les regrouper dans une même interface/classe est une erreur commune. Et c'est normal, on nous répète souvent DRY(Do n't Repeat Yourself) et on a envie de factoriser les utilisateurs dans une même classe ou dans une même interface pour y appliquer des méthodes communes.

Et si on faisait le contraire ? Trois 유형 d'utilisateurs, donc trois 인터페이스.

interface GuestUser {
  userType: "Guest";
}

interface CustomerUser {
  userType: "Customer";
  login: string;
}

interface AdminUser {
  userType: "Admin";
  login: string;
  accessRights: string[];
}


Ensuite, il nous suffit de définir un type qui 해당 à l'union des trois 인터페이스 구별:

type User = GuestUser | CustomerUser | AdminUser;


Cette fois, la syntaxe nous permet toujours de créer des utilisateurs valides:

const johnDoe: User = {
  userType: "Admin",
  login: "JohnDoe",
  accessRights: ["database", "monitoring"],
};
const customer: User = {
  userType: "Customer",
  login: "JaneDoe",
};
const guest: User = {
  userType: "Guest",
};


Mais interdit la création d'utilisateurs qui n'ont pas de sens :

/**
 * Type '{ userType: "Customer"; }' is not assignable to type 'User'.
 * Property 'login' is missing in type '{ userType: "Customer"; }'
 * but required in type 'CustomerUser'.
 */
const customerWithoutLogin: User = {
  userType: "Customer",
};

/**
 * Type '{ userType: "Guest"; accessRights: string[]; }'is not assignable to type 'User'.
 * Object literal may only specify known properties,
 * and 'accessRights' does not exist in type 'GuestUser'.
 */
const guestWithAccessRights: User = {
  userType: "Guest",
  accessRights: ["fs"],
};


L'accès aux propriétés est également bien plus predictible :

johnDoe.login.toUpperCase(); // OK
customer.login.toUpperCase(); // OK
guest.login.toUpperCase(); // Property 'login' does not exist on type 'GuestUser'.


L'inférence de fait également des merveilles 유형:

// login is defined because GuestUser is excluded (Type guard)
const displayLogin = (user: User) =>
  user.userType === "Guest" ? "Guest" : user.login;


ProTip: Si l'inférence ne fonctionne pas, pensez à définir un champ qui va aider Typescript à determiner le bon type( userType dans notre exemple). Vous pouvez aussi 식별자 le type manuellement avec le mot-clé is .

const isAdmin = (user: User): user is AdminUser =>
  (user as AdminUser).accessRights !== undefined;

const users: User[] = [johnDoe, customer, guest];

users.filter(isAdmin).forEach((admin) => console.log(admin.accessRights));


결론



Vous pouvez maintenant être plus précis sur le typage des données. Rien de révolutionnaire ici mais rappelez-vous que le typage est un bon moyen d'augmenter la prédictibilité de votre code.

알러 플러스 로스



Je vous Invitation à aller voir mon article sur le pattern matching en JS (qui arrive bientôt) qui complète assez bien les unions disjointes que l'on vient de voir. En combinant les deux, vous pouvez notamment faire une sorte de polymorphisme sans héritage et sans classe.

const redirectToHomePage = () => (location.href = "/");
const redirectToAccountPage = () => (location.href = "/account");
const redirectToAdminDashboardPage = () => (location.href = "/admin/dashboard");

export const redirectToUserPage = (user: User) =>
  ({
    Guest: () => redirectToHomePage(),
    Customer: () => redirectToAccountPage(),
    Admin: () => redirectToAdminDashboardPage(),
  }[user.userType]());



Un article également disponible sur :
  • le blog Younup : https://www.younup.fr/blog/exploiter-les-discriminated-unions-en-typescript
  • 월 블로그 : https://brack0.dev/fr/blog/ts-discriminated-union
  • 좋은 웹페이지 즐겨찾기