JavaScript Development Space

How to Use the Never Type Like a Pro in TypeScript

TypeScript, as a superset of JavaScript, enhances the development experience by adding static types. Among its powerful features is the never type, which is often misunderstood or underutilized. This article will explore what the never type is, when to use it, and some best practices to help you leverage it like a pro.

What is the Never Type?

The never type in TypeScript represents values that never occur. This can happen in two primary situations:

  • Function that Throws an Error: A function that always throws an error or never returns a value.
  • Function that Never Finishes: A function that runs indefinitely, such as an infinite loop.

Example of the Never Type

Here’s a simple example to illustrate the never type:

ts
1 function throwError(message: string): never {
2 throw new Error(message);
3 }
4
5 function infiniteLoop(): never {
6 while (true) {
7 // This loop never ends
8 }
9 }

In the above examples, throwError always throws an error, and infiniteLoop runs indefinitely, so they both return never.

When to Use the Never Type

Understanding when to use the never type is crucial for writing robust TypeScript code. Here are some common scenarios:

1. Exhaustiveness Checking in Switch Statements

When you have a union type, you can use the never type to ensure that all cases are handled. If a switch statement does not handle all possible cases, TypeScript will throw an error.

ts
1 type Shape = 'circle' | 'square';
2
3 function area(shape: Shape): number {
4 switch (shape) {
5 case 'circle':
6 return Math.PI * 10 * 10; // example for a circle
7 case 'square':
8 return 10 * 10; // example for a square
9 default:
10 // This will cause a compile error if new shapes are added to Shape
11 const _exhaustiveCheck: never = shape;
12 return _exhaustiveCheck;
13 }
14 }

In this example, if you add a new shape to the Shape type but forget to update the area function, TypeScript will alert you with a compile-time error.

2. Handling Unreachable Code

You can use the never type in functions that are meant to handle unexpected values. This is particularly useful in scenarios involving type guards.

ts
1 function handleInput(input: string | number) {
2 if (typeof input === 'string') {
3 console.log(`String: ${input}`);
4 } else if (typeof input === 'number') {
5 console.log(`Number: ${input}`);
6 } else {
7 // TypeScript will ensure that this code is unreachable
8 const _: never = input; // Error if input is neither string nor number
9 }
10 }

Here, if the input variable is anything other than a string or number, TypeScript will throw an error, ensuring all cases are considered.

3. Custom Error Handling

You can also use the never type to define functions that always throw errors without returning a value. This is helpful for creating robust error handling.

js
1 function assertIsDefined<T>(value: T | undefined, message: string): T {
2 if (value === undefined) {
3 throw new Error(message);
4 }
5 return value;
6 }

In this case, if value is undefined, an error is thrown, and the function does not return anything.

Advanced Examples

Some advanced examples

Example 1

ts
1 export const example1 = () => {
2 let logValue = '';
3
4 const createSomeDesc = (value: string | number | object): string | never => {
5 switch (typeof value) {
6 case 'string':
7 return 'log string';
8 case 'number':
9 return 'log number';
10 default:
11 throw new Error('error in createSomeDesc');
12 }
13 };
14
15 logValue = 'some string' + createSomeDesc({});
16 };
17
18 it('example1', () => {
19 expect(() => {
20 example1();
21 }).toThrow();
22 });

Key Points:

  • Never as a subtype: The never type is a subtype of every type, so according to the Liskov Substitution Principle, assigning never to any type is safe:
ts
1 type TValue = string | never extends string ? true : false; // true
  • The createSomeDesc function throws an exception if the parameter is neither a string nor a number.

  • Assigning a new value to logValue is an unreachable operation, which clearly demonstrates incorrect behavior.

This example shows what happens when the parameter type is extended without providing an implementation for object. The return type of string | never is included for clarity.

Although this behavior in TypeScript might seem dangerous, it allows for free usage of code inside try/catch blocks. Comprehensive type descriptions, as described in the documentation, become necessary to control situations at the TypeScript level.

Type Expressions: Example 2

Using never is key to creating utility types.

ts
1 type GenericWithRestriction<T extends string> = T;
2 type GenericWithNever<T> = T extends string ? T : never;
3
4 const neverAgainEx2 = () => {
5 const value: GenericWithRestriction<string> = '';
6 //@ts-ignore
7 const neverValue: GenericWithNever<number> = ''; // TS2322: Type string is not assignable to type never
8 const value2: GenericWithNever<string> = '';
9 };

TypeScript "eliminates" any type that can lead to never. This allows us to use only types that make sense.

Examples of Using Never in Expressions

Consider the following example:

ts
1 const messages = {
2 defaultPrompt: {
3 ok: 'Ok',
4 cancel: 'Cancel',
5 },
6 defaultAction: {
7 file: {
8 rm: 'delete file',
9 create: 'create file',
10 },
11 directory: {
12 rm: 'delete directory',
13 create: 'make directory',
14 },
15 },
16 title1: 'default title 1',
17 };
18
19 export const getMessageByKey = (key: string): string => eval(`messages.${key}`);

Task: Configure the type of getMessageByKey so that key accepts strings in the format of path.to.value. The implementation itself does not matter.

We will turn messages into a literal using as const.

Option 1:

ts
1 type KeyTree = {
2 [key: string]: string | KeyTree;
3 };
4
5 type TExtractAllKeysTypeA<O extends KeyTree, K extends keyof O = keyof O> = K extends string
6 ? O[K] extends KeyTree
7 ? `${K}.${TExtractAllKeysTypeA<O[K]>}`
8 : K
9 : never;

Key Points:

  • K extends string serves two functions:
  • Allows working with the distributivity of unions concerning the extends operation.
  • Narrows down the set of keys by excluding symbol, which will be useful for template string literals.
  • To define keys in the format of path.to.property, we use template string literals.
  • Recursion is used to create a set of all keys.
  • For ease of use, we set a default value for the second generic type.

In this case, the explicit use of never plays a modest role, filtering out symbol from the set of keys keyof O. However, there is also implicit behavior: for key values other than string | KeyTree, the expression ${K}.${TExtractAllKeysTypeA<O[K]>} will resolve to never, thereby excluding such keys. The utility can be transformed into:

ts
1 type TExtractAllKeysTypeA<O, K extends keyof O = keyof O> = K extends string
2 ? O[K] extends string
3 ? K
4 : `${K}.${TExtractAllKeysTypeA<O[K]>}`
5 : never;

Of course, in this case, the literal messages is not controlled in any way.

Final Result:

ts
1 export const getMessageByKey = (key: TExtractAllKeysTypeA<typeof messages>): string => eval(`messages.${key}`);

Option 2:

ts
1 type TExtractAllKeysTypeB<O> = {
2 [K in keyof O]: K extends string
3 ? O[K] extends string
4 ? K
5 : `${K}.${TExtractAllKeysTypeB<O[K]>}`
6 : never;
7 }[keyof O];
  • The number of generics has been reduced to one.
  • never is used in a more inventive way. TypeScript eliminates properties whose values are never.
  • Implicit conversion to never is utilized.

Finally, we can consider a function that works with any messages:

ts
1 const _getMessageByKeyTypeA = <T extends KeyTree>(data: T) => {
2 return (key: TExtractAllKeysTypeA<T>): string => eval(`data.${String(key)}`);
3 };
4
5 const _getMessageByKeyTypeB = <T>(data: T) => {
6 return (key: TExtractAllKeysTypeB<T>): string => eval(`data.${String(key)}`);
7 };
8
9 export const getMessageByKeyTypeA = _getMessageByKeyTypeA(messages);
10 export const getMessageByKeyTypeB = _getMessageByKeyTypeB(messages);

Best Practices for Using the Never Type

To use the never type effectively, consider the following best practices:

  • Leverage Exhaustiveness Checking: Use never in switch statements or conditional statements to ensure all possible cases are handled.

  • Be Clear with Intent: Use never to explicitly indicate that certain functions are not expected to return. This enhances readability and helps other developers understand your code's intention.

  • Use Type Guards Wisely: Implement never in type guards to enforce handling of all cases. This ensures your code is safe and robust.

  • Document Your Functions: When using never, add comments or documentation to clarify why a function does not return. This will help maintainers understand your code better.

  • Avoid Overusing: While never can be powerful, overusing it in simple scenarios can lead to confusion. Use it where appropriate, but keep your codebase maintainable.

Conclusion

The never type in TypeScript is a powerful feature that can help you write safer, more predictable code. By understanding when and how to use it, you can enhance your TypeScript skills and develop robust applications.

With the tips and best practices outlined in this article, you’re well-equipped to leverage the never type like a pro. Happy coding!

JavaScript Development Space

© 2024 JavaScript Development Space - Master JS and NodeJS. All rights reserved.