TypeScript Power never

When I first saw the word never, I thought how useless the type appeared in TypeScript. Over time, sinking deeper into ts, I began to understand how powerful this word is. And this power is born from real-world use cases that I intend to share with the reader. Who cares, welcome to cat.



What is never



If you plunge into history, we will see that the type never appeared at the dawn of TypeScript version 2.0, with a rather modest description of its purpose. If you briefly and freely retell the version of the ts developers, then the type never is a primitive type that represents a sign for values ​​that will never happen. Or, a sign for functions that will never return values, either because of its loop, for example, an infinite loop, or because of its interruption. And in order to clearly show the essence of what was said, I propose to see an example below:



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





Perhaps because of such examples, I got the first impression that the type is needed for clarity.



Type system



Now I can say that the rich fauna of the type system in TypeScript as well is the merit of never. And in support of my words I will give several library types from 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;
      
      





Of my types with never, I’ll give my favorite - GetNames, an improved analogue of 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>;
      
      





Monitoring future changes



When the life of the project is not fleeting, the development takes on a special character. I would like to have control over key points in the code, to have guarantees that you or your team members will not forget to fix a vital piece of code when it takes time. Such sections of code are especially effective when there is a state or enumeration of something. For example, listing a set of actions on an entity. I suggest a look at an example to understand the context of what is happening.



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





The code above is simplified as much as possible only in order to focus on an important point - the AdminAction type is defined in another project and it is even possible that it is not accompanied by your team. Since the project will live for a long time, it is necessary to protect your ActionEngine from changes in the AdminAction type without your knowledge. TypeScript offers several recipes to solve this problem, one of which is to use the never type. To do this, we need to define a NeverError and use it in the doAction method.



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





Now add a new “BLOCK” value to AdminAction and get an error at compile time: Argument of type '“BLOCK”' is not assignable to parameter of type 'never'.ts (2345).



In principle, we achieved this. It is worth mentioning an interesting point that the switch construct protects us from changing AdminAction elements or removing them from the set. From practice, I can say that it really works as expected.



If you don’t want to introduce the NeverError class, you can control the code by declaring a variable of type never. Like this:



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





Context Limit: this + never



The following trick often saves me from ridiculous mistakes amid fatigue or carelessness. In the example below, I will not give an assessment of the quality of the chosen approach. With us, de facto, this happens. Suppose you use a method in a class that does not have access to the fields of the class. Yes, that sounds scary - all this is gov ... code.



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





In a wider case, it can be callback or arrow functions, which have their own execution contexts. And the task is: How to protect yourself from runtime error? TypeScript has the ability to specify this context.



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





In fairness, I’ll say that it’s never merit. Instead, you can use void and {}. But it is the type never that attracts attention when you read the code.



Expectations



Invariants



Having a definite idea of ​​never, I thought the following code should work:



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





But alas. The expression (x + 1) throws an error: Operator '+' cannot be applied to types 'Maybe' and '1'. The example itself I spied in the article Transferring 30,000 lines of code from Flow to TypeScript.



Flexible binding



I thought that with the help of never I can control the mandatory function parameters and, under certain conditions, disable unnecessary ones. But no, that won't work:



 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)
      
      





The above problem is solved in a different way .



More stringent verification



I wanted the ts compiler not to miss something like that contrary to common sense.



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





Conclusion



In the end, I want to offer a small task, from a series of strange oddities. Which line is the error?



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





Option
In the latter: Type '""' is not assignable to type 'never'.ts (2322)



That's all I wanted to tell about never. Thank you all for your attention and see you soon.



All Articles