TypeScript Puissance jamais

Quand j'ai vu le mot jamais pour la première fois, j'ai pensé à quel point le type était inutile dans TypeScript. Au fil du temps, plongeant plus profondément dans les ts, j'ai commencé à comprendre à quel point ce mot est puissant. Et ce pouvoir est né de cas d'utilisation réels que j'ai l'intention de partager avec le lecteur. Peu importe, bienvenue au chat.

Ce qui n'est jamais


Si vous plongez dans l'histoire, nous verrons que le type n'est jamais apparu à l'aube de TypeScript version 2.0, avec une description plutôt modeste de son objectif. Si vous expliquez brièvement et librement la version des développeurs ts, le type ne sera jamais un type primitif qui représente un signe de valeurs qui ne se produiront jamais. Ou, un signe pour des fonctions qui ne renverront jamais de valeurs, soit à cause de sa boucle, par exemple une boucle infinie, soit à cause de son interruption. Et afin de montrer clairement l'essence de ce qui a été dit, je propose de voir un exemple ci-dessous:

/**    */ function error(message: string): never { throw new Error(message); } /**   */ function infiniteLoop(): never { while (true) { } } /**   */ function infiniteRec(): never { return infiniteRec(); } 

Peut-être à cause de tels exemples, j'ai eu la première impression que le type est nécessaire pour plus de clarté.

Système de type


Maintenant, je peux dire que la faune du système de type riche dans TypeScript n'est jamais due non plus. Et à l'appui de mes mots, je donnerai plusieurs types de bibliothèques de lib.es5.d.ts

 /** Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T; /** Extract from T those types that are assignable to U */ type Extract<T, U> = T extends U ? T : never; /** Construct a type with the properties of T except for those in type K. */ type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; /** Exclude null and undefined from T */ type NonNullable<T> = T extends null | undefined ? never : T; /** Obtain the parameters of a function type in a tuple */ type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; 

De mes types avec jamais, je donnerai mon préféré - GetNames, un analogue amélioré de keyof:

 /** * GetNames      * @template FromType  -   * @template KeepType   * @template Include       .   false -    KeepType */ type GetNames<FromType, KeepType = any, Include = true> = { [K in keyof FromType]: FromType[K] extends KeepType ? Include extends true ? K : never : Include extends true ? never : K }[keyof FromType]; //   class SomeClass { firstName: string; lastName: string; age: number; count: number; getData(): string { return "dummy"; } } // be: "firstName" | "lastName" type StringKeys = GetNames<SomeClass, string>; // be: "age" | "count" type NumberKeys = GetNames<SomeClass, number>; // be: "getData" type FunctionKeys = GetNames<SomeClass, Function>; // be: "firstName" | "lastName" | "age" | "count" type NonFunctionKeys = GetNames<SomeClass, Function, false>; 

Suivi des changements futurs


Lorsque la vie du projet n'est pas éphémère, le développement prend un caractère particulier. Je voudrais avoir le contrôle sur les points clés du code, avoir la garantie que vous ou les membres de votre équipe n'oublierez pas de corriger un morceau de code essentiel quand cela prend du temps. De telles sections de code sont particulièrement manifestes lorsqu'il existe un état ou une énumération de quelque chose. Par exemple, répertorier un ensemble d'actions sur une entité. Je suggère de regarder un exemple pour comprendre le contexte de ce qui se passe.

 //    -   ,     ,   ActionEngine type AdminAction = "CREATE" | "ACTIVATE"; // ,     ,   AdminAction   . class ActionEngine { doAction(action: AdminAction) { switch (action) { case "CREATE": //   return "CREATED"; case "ACTIVATE": //   return "ACTIVATED"; default: throw new Error("   "); } } } 

Le code ci-dessus est simplifié autant que possible uniquement afin de se concentrer sur un point important - le type AdminAction est défini dans un autre projet et il est même possible qu'il ne soit pas accompagné par votre équipe. Comme le projet va durer longtemps, il est nécessaire de protéger votre ActionEngine contre les modifications du type AdminAction à votre insu. TypeScript propose plusieurs recettes pour résoudre ce problème, dont l'une consiste à utiliser le type Never. Pour ce faire, nous devons définir un NeverError et l'utiliser dans la méthode doAction.

 class NeverError extends Error { //         - ts   constructor(value: never) { super(`Unreachable statement: ${value}`); } } class ActionEngine { doAction(action: AdminAction) { switch (action) { case "CREATE": //   return "CREATED"; case "ACTIVATE": //   return "ACTIVATED"; default: throw new NeverError(action); // ^       switch  . } } } 

Ajoutez maintenant une nouvelle valeur "BLOC" à AdminAction et obtenez une erreur au moment de la compilation: l'argument de type "" BLOC "" n'est pas attribuable au paramètre de type "jamais" .ts (2345).

En principe, nous y sommes parvenus. Il convient de mentionner un point intéressant: la construction du commutateur nous protège de la modification des éléments AdminAction ou de leur suppression de l'ensemble. De la pratique, je peux dire que cela fonctionne vraiment comme prévu.

Si vous ne souhaitez pas introduire la classe NeverError, vous pouvez contrôler le code en déclarant une variable de type never. Comme ça:

 type AdminAction = "CREATE" | "ACTIVATE" | "BLOCK"; class ActionEngine { doAction(action: AdminAction) { switch (action) { case "CREATE": //   return "CREATED"; case "ACTIVATE": //   return "ACTIVATED"; default: const unknownAction: never = action; // Type '"BLOCK"' is not assignable to type 'never'.ts(2322) throw new Error(`   ${unknownAction}`); } } } 

Limite de contexte: ce + jamais


L'astuce suivante me sauve souvent des erreurs ridicules au milieu de la fatigue ou de la négligence. Dans l'exemple ci-dessous, je ne donnerai pas d'évaluation de la qualité de l'approche choisie. Avec nous, de facto, cela se produit. Supposons que vous utilisez une méthode dans une classe qui n'a pas accès aux champs de la classe. Oui, cela semble effrayant - tout cela est gov ... code.

 @SomeDecorator({...}) class SomeUiPanel { @Inject private someService: SomeService; public beforeAccessHook() { //    ,    ,     SomeUiPanel this.someService.doInit("Bla bla"); // ^       :  beforeAccessHook   ,      } } 

Dans un cas plus large, il peut s'agir de fonctions de rappel ou de flèche, qui ont leurs propres contextes d'exécution. Et la tâche est la suivante: comment vous protéger contre les erreurs d'exécution? Pour cela, TypeScript a la possibilité de spécifier ce contexte.

 @SomeDecorator({...}) class SomeUiPanel { @Inject private someService: SomeService; public beforeAccessHook(this: never) { //    ,    ,     SomeUiPanel this.someService.doInit("Bla bla"); // ^ Property 'someService' does not exist on type 'never' } } 

En toute honnêteté, je dirai que ce n'est jamais valable. À la place, vous pouvez utiliser void et {}. Mais c'est le type jamais qui attire l'attention lorsque vous lisez le code.

Les attentes


Invariants


Ayant une idée précise de jamais, je pensais que le code suivant devrait fonctionner:

 type Maybe<T> = T | void; function invariant<Cond extends boolean>(condition: Cond, message: string): Cond extends true ? void : never { if (condition) { return; } throw new Error(message); } function f(x: Maybe<number>, c: number) { if (c > 0) { invariant(typeof x === "number", "When c is positive, x should be number"); (x + 1); // works because x has been refined to "number" } } 

Mais hélas. L'expression (x + 1) renvoie une erreur: l'opérateur '+' ne peut pas être appliqué aux types 'Peut-être' et '1'. L'exemple lui-même que j'ai espionné dans l'article Transfert de 30 000 lignes de code de Flow vers TypeScript.

Reliure flexible


Je pensais qu'avec l'aide de Never, je pouvais contrôler les paramètres de fonction obligatoires et, dans certaines conditions, désactiver ceux qui n'étaient pas nécessaires. Mais non, cela ne fonctionnera pas:

 function variants<Type extends number | string>(x: Type, c: Type extends number ? number : never): number { if (typeof x === "number") { return x + c; } return +x; } const three = variants(1, 2); // ok // 2  - never,     string. ,   const one = variants("1"); // expected 2 arguments, but got 1.ts(2554) 

Le problème ci-dessus est résolu d'une manière différente .

Vérification plus stricte


Je voulais que le compilateur ts ne manque pas quelque chose comme ça contrairement au bon sens.

 variants(<never> {}, <never> {}); 

Conclusion


Au final, je veux vous proposer une petite tâche, à partir d'une série d'étranges bizarreries. Quelle ligne est l'erreur?

 class never<never> { never: never; } const whats = new never<string>(); whats.never = ""; 

Option
Dans ce dernier: le type '""' n'est pas attribuable au type 'never'.ts (2322)

C'est tout ce que je voulais dire jamais. Merci à tous pour votre attention et à bientôt.

Source: https://habr.com/ru/post/fr471026/


All Articles