TypeScript Poder nunca

Quando vi a palavra nunca, pensei em como o tipo era inútil no TypeScript. Com o tempo, afundando cada vez mais em ts, comecei a entender o quão poderosa essa palavra é. E esse poder nasce dos casos de uso do mundo real que pretendo compartilhar com o leitor. Quem se importa, bem-vindo ao gato.

O que nunca é


Se mergulharmos na história, veremos que o tipo nunca apareceu no início do TypeScript versão 2.0, com uma descrição bastante modesta de sua finalidade. Se você recontar breve e livremente a versão dos desenvolvedores ts, o tipo nunca será um tipo primitivo que representa um sinal para valores que nunca acontecerão. Ou, um sinal para funções que nunca retornarão valores, seja por causa de seu loop, por exemplo, um loop infinito, seja por causa de sua interrupção. E para mostrar claramente a essência do que foi dito, proponho ver um exemplo abaixo:

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

Talvez por causa desses exemplos, tive a primeira impressão de que o tipo é necessário para maior clareza.

Sistema de tipos


Agora posso dizer que a rica fauna do sistema de tipos no TypeScript também nunca é devida. E em apoio às minhas palavras, darei vários tipos de bibliotecas em 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; 

Dos meus tipos com nunca, darei o meu favorito - GetNames, um análogo aprimorado do 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>; 

Monitorando mudanças futuras


Quando a vida do projeto não é passageira, o desenvolvimento assume um caráter especial. Eu gostaria de ter controle sobre os principais pontos do código, ter garantias de que você ou os membros da sua equipe não esquecerão de corrigir um trecho de código vital quando levar algum tempo. Tais seções do código são particularmente manifestas quando há um estado ou enumeração de algo. Por exemplo, listando um conjunto de ações em uma entidade. Sugiro olhar um exemplo para entender o contexto do que está acontecendo.

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

O código acima é simplificado o máximo possível apenas para se concentrar em um ponto importante - o tipo AdminAction é definido em outro projeto e é até possível que não seja acompanhado por sua equipe. Como o projeto permanecerá por muito tempo, é necessário proteger seu ActionEngine de alterações no tipo AdminAction sem o seu conhecimento. O TypeScript oferece várias receitas para resolver esse problema, uma das quais é usar o tipo never. Para fazer isso, precisamos definir um NeverError e usá-lo no 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  . } } } 

Agora adicione um novo valor "BLOCK" ao AdminAction e obtenha um erro no tempo de compilação: o argumento do tipo '"BLOCK"' não pode ser atribuído ao parâmetro do tipo 'never'.ts (2345).

Em princípio, conseguimos isso. Vale mencionar um ponto interessante de que a construção do switch nos protege de alterar os elementos AdminAction ou removê-los do conjunto. Da prática, posso dizer que realmente funciona como esperado.

Se você não deseja introduzir a classe NeverError, pode controlar o código declarando nunca uma variável do tipo. Assim:

 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 contexto: isso + nunca


O truque a seguir muitas vezes me salva de erros ridículos em meio a fadiga ou descuido. No exemplo abaixo, não darei uma avaliação da qualidade da abordagem escolhida. Com a gente, de fato, isso acontece. Suponha que você use um método em uma classe que não tenha acesso aos campos da classe. Sim, parece assustador - tudo isso é código gov ...

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

Em um caso mais amplo, podem ser funções de retorno de chamada ou seta, que possuem seus próprios contextos de execução. E a tarefa é: Como se proteger de erros de tempo de execução? Para isso, o TypeScript tem a capacidade de especificar esse 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 justo, direi que nunca é mérito. Em vez disso, você pode usar void e {}. Mas é o tipo que nunca chama a atenção quando você lê o código.

Expectativas


Invariantes


Tendo uma ideia definida de nunca, pensei que o seguinte código deveria 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" } } 

Mas infelizmente. A expressão (x + 1) gera um erro: O operador '+' não pode ser aplicado aos tipos 'Maybe' e '1'. O exemplo em si que espiei no artigo Transferindo 30.000 linhas de código do Flow para o TypeScript.

Encadernação flexível


Eu pensei que, com a ajuda de never, eu posso controlar os parâmetros de função obrigatórios e, sob certas condições, desativar os desnecessários. Mas não, isso não vai 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) 

O problema acima é resolvido de uma maneira diferente .

Verificação mais rigorosa


Eu queria que o compilador ts não perdesse algo assim contrário ao senso comum.

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

Conclusão


No final, quero oferecer uma pequena tarefa, de uma série de estranhezas estranhas. Qual linha é o erro?

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

Opção
No último: o tipo '""' não é atribuível ao tipo 'never'.ts (2322)

Isso é tudo que eu queria contar sobre nunca. Obrigado a todos pela atenção e até breve.

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


All Articles