TypeScript Poder nunca

Cuando vi por primera vez la palabra nunca, pensé en lo inútil que aparecía el tipo en TypeScript. Con el tiempo, al sumergirme más en ts, comencé a comprender cuán poderosa es esta palabra. Y este poder nace de casos de uso del mundo real que pretendo compartir con el lector. A quién le importa, bienvenido al gato.

Lo que nunca es


Si nos sumergimos en la historia, veremos que el tipo nunca apareció en los albores de TypeScript versión 2.0, con una descripción bastante modesta de su propósito. Si vuelve a contar breve y libremente la versión de los desarrolladores ts, entonces el tipo nunca es un tipo primitivo que representa un signo de valores que nunca sucederán. O bien, un signo de funciones que nunca devolverán valores, ya sea por su bucle, por ejemplo, un bucle infinito o por su interrupción. Y para mostrar claramente la esencia de lo que se dijo, propongo ver un ejemplo a continuación:

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

Quizás debido a tales ejemplos, tuve la primera impresión de que el tipo es necesario para mayor claridad.

Sistema de tipo


Ahora puedo decir que la rica fauna del sistema de tipos en TypeScript nunca se debe. Y en apoyo de mis palabras, daré varios tipos de bibliotecas 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 mis tipos con nunca, daré mi favorito: GetNames, un análogo mejorado 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>; 

Monitorear cambios futuros


Cuando la vida del proyecto no es fugaz, el desarrollo adquiere un carácter especial. Me gustaría tener control sobre los puntos clave en el código, para tener garantías de que usted o los miembros de su equipo no olvidarán arreglar un código vital cuando lleve tiempo. Tales secciones de código son particularmente manifiestas cuando hay un estado o enumeración de algo. Por ejemplo, enumerar un conjunto de acciones en una entidad. Sugiero mirar un ejemplo para comprender el contexto de lo que está sucediendo.

 //    -   ,     ,   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("   "); } } } 

El código anterior se simplifica tanto como sea posible solo para centrarse en un punto importante: el tipo de AdminAction se define en otro proyecto e incluso es posible que su equipo no lo acompañe. Como el proyecto durará mucho tiempo, es necesario proteger su ActionEngine de los cambios en el tipo de AdminAction sin su conocimiento. TypeScript ofrece varias recetas para resolver este problema, una de las cuales es usar el tipo nunca. Para hacer esto, necesitamos definir NeverError y usarlo en el método 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  . } } } 

Ahora agregue un nuevo valor "BLOCK" a AdminAction y obtenga un error en el momento de la compilación: el argumento del tipo '"BLOCK"' no se puede asignar al parámetro del tipo 'never'.ts (2345).

En principio, lo logramos. Vale la pena mencionar un punto interesante de que la construcción del interruptor nos protege de cambiar los elementos de AdminAction o eliminarlos del conjunto. Desde la práctica, puedo decir que realmente funciona como se esperaba.

Si no desea introducir la clase NeverError, puede controlar el código declarando una variable de tipo nunca. Así:

 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}`); } } } 

Límite de contexto: esto + nunca


El siguiente truco a menudo me salva de errores ridículos en medio de la fatiga o el descuido. En el siguiente ejemplo, no daré una evaluación de la calidad del enfoque elegido. Con nosotros, de hecho, esto sucede. Suponga que usa un método en una clase que no tiene acceso a los campos de la clase. Sí, suena aterrador, todo esto es código gubernamental ...

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

En un caso más amplio, pueden ser funciones de devolución de llamada o flecha, que tienen sus propios contextos de ejecución. Y la tarea es: ¿Cómo protegerse de los errores de tiempo de ejecución? Para esto, TypeScript tiene la capacidad de especificar este contexto.

 @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' } } 

Para ser justos, diré que nunca vale la pena. En su lugar, puede usar void y {}. Pero es el tipo que nunca llama la atención cuando lees el código.

Las expectativas


Invariantes


Teniendo una idea definitiva de nunca, pensé que el siguiente código debería funcionar:

 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" } } 

Pero por desgracia. La expresión (x + 1) arroja un error: el operador '+' no se puede aplicar a los tipos 'Quizás' y '1'. El ejemplo en sí lo vi en el artículo Transfiriendo 30,000 líneas de código de Flow a TypeScript.

Encuadernación flexible


Pensé que, con la ayuda de Never, puedo controlar los parámetros de la función obligatoria y, bajo ciertas condiciones, deshabilitar los innecesarios. Pero no, eso no funcionará:

 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) 

El problema anterior se resuelve de una manera diferente .

Verificación más estricta


Quería que el compilador ts no se perdiera algo así contrario al sentido común.

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

Conclusión


Al final, quiero ofrecer una pequeña tarea, de una serie de extrañas rarezas. ¿Qué línea es el error?

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

Opcion
En el último: el tipo '""' no se puede asignar al tipo 'never'.ts (2322)

Eso es todo lo que quería contar sobre nunca. Gracias a todos por su atención y hasta pronto.

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


All Articles