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:
1 function throwError(message: string): never {2 throw new Error(message);3 }45 function infiniteLoop(): never {6 while (true) {7 // This loop never ends8 }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.
1 type Shape = 'circle' | 'square';23 function area(shape: Shape): number {4 switch (shape) {5 case 'circle':6 return Math.PI * 10 * 10; // example for a circle7 case 'square':8 return 10 * 10; // example for a square9 default:10 // This will cause a compile error if new shapes are added to Shape11 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.
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 unreachable8 const _: never = input; // Error if input is neither string nor number9 }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.
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
1 export const example1 = () => {2 let logValue = '';34 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 };1415 logValue = 'some string' + createSomeDesc({});16 };1718 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:
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.
1 type GenericWithRestriction<T extends string> = T;2 type GenericWithNever<T> = T extends string ? T : never;34 const neverAgainEx2 = () => {5 const value: GenericWithRestriction<string> = '';6 //@ts-ignore7 const neverValue: GenericWithNever<number> = ''; // TS2322: Type string is not assignable to type never8 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:
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 };1819 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:
1 type KeyTree = {2 [key: string]: string | KeyTree;3 };45 type TExtractAllKeysTypeA<O extends KeyTree, K extends keyof O = keyof O> = K extends string6 ? O[K] extends KeyTree7 ? `${K}.${TExtractAllKeysTypeA<O[K]>}`8 : K9 : 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:
1 type TExtractAllKeysTypeA<O, K extends keyof O = keyof O> = K extends string2 ? O[K] extends string3 ? K4 : `${K}.${TExtractAllKeysTypeA<O[K]>}`5 : never;
Of course, in this case, the literal messages
is not controlled in any way.
Final Result:
1 export const getMessageByKey = (key: TExtractAllKeysTypeA<typeof messages>): string => eval(`messages.${key}`);
Option 2:
1 type TExtractAllKeysTypeB<O> = {2 [K in keyof O]: K extends string3 ? O[K] extends string4 ? K5 : `${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
:
1 const _getMessageByKeyTypeA = <T extends KeyTree>(data: T) => {2 return (key: TExtractAllKeysTypeA<T>): string => eval(`data.${String(key)}`);3 };45 const _getMessageByKeyTypeB = <T>(data: T) => {6 return (key: TExtractAllKeysTypeB<T>): string => eval(`data.${String(key)}`);7 };89 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!