TypeScript The unknown and never Types Step by step Implementation and Top 10 Questions and Answers
 .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    Last Update: April 01, 2025      22 mins read      Difficulty-Level: beginner

TypeScript: The unknown and never Types

TypeScript, a popular statically typed programming language built on JavaScript, provides several built-in types to help ensure type safety and correctness in your code. Two of these types are unknown and never, which might initially seem similar due to their restrictive nature, but they serve distinct purposes and are used in different scenarios.

Understanding the unknown Type

The unknown type was introduced in TypeScript 3.0 as a secure type for variables that can hold any value, much like the any type. However, there's a crucial difference: operations that you perform on an unknown variable are strictly limited until you've narrowed down its type.

Here's a detailed breakdown:

  1. Definition and Usage:

    • The unknown type is used when you do not know the type of the variable beforehand. It allows you to assign it any value, just like any.
    let userInput: unknown;
    userInput = 5; // Valid
    userInput = 'Hello'; // Valid
    userInput = true; // Valid
    
  2. Strict Type Checking:

    • Unlike any, you cannot directly perform operations on unknown variables without first checking or narrowing them to a more specific type.
    let userInput: unknown;
    userInput = 'Some string';
    
    // Error: Property 'toUpperCase' does not exist on type 'unknown'.
    console.log((userInput as string).toUpperCase());
    
    • This means that even if you know that userInput will be a string eventually, TypeScript forces you to perform a type check or assertion before using it as such.
    let userInput: unknown;
    userInput = 'Some string';
    
    if (typeof userInput === 'string') {
        // Now TypeScript knows that userInput is a string
        console.log(userInput.toUpperCase()); // Valid
    }
    
  3. Benefits Over any:

    • Using unknown instead of any adds an extra layer of type safety by requiring explicit type checks or assertions. This reduces the risk of runtime errors by preventing unsafe operations on variables whose types are uncertain.
  4. Type Narrowing:

    • Type narrowing is a technique where TypeScript figures out the most specific possible type for a variable within a given scope, based on control flow and type checks.
    function logValue(val: unknown) {
        if (typeof val === 'string') {
            console.log(val.toUpperCase());
        } else if (typeof val === 'number') {
            console.log(val.toExponential());
        } else {
            console.log(`Value is neither a string nor a number`);
        }
    }
    

Understanding the never Type

The never type represents values that never occur. It is typically used in scenarios where a function will never return normally, throwing an exception or terminating the program. Unlike unknown, never has no valid assignments, not even null or undefined.

Here's a detailed exploration:

  1. Definition and Usage:

    • The never type is used to denote a function that does not return a value or returns a value of type never.
    function throwError(message: string): never {
        throw new Error(message);
    }
    
  2. Functions That Never Return:

    • Functions that always throw exceptions return never. Similarly, functions that have infinite loops do not return a value and thus also return never.
    function infiniteLoop(): never {
        while (true) {
            // Loop indefinitely without returning
        }
    }
    
  3. Assignment Rules:

    • The never type can only be assigned to values of type never. Any other assignment results in a type error.
    let x: never;
    
    // All the following assignments would result in a type error
    // x = 5; // Error: Type '5' is not assignable to type 'never'.
    // x = 'Hello'; // Error: Type '"Hello"' is not assignable to type 'never'.
    
  4. Use with Type Guards:

    • The never type is often used in type guards and exhaustive switch-case statements. It ensures that all possible cases are handled, thus catching any unexpected types at compile time.
    type Direction = 'north' | 'south' | 'east' | 'west';
    
    function move(direction: Direction) {
        switch (direction) {
            case 'north':
                console.log('Moving north');
                break;
            case 'south':
                console.log('Moving south');
                break;
            case 'east':
                console.log('Moving east');
                break;
            case 'west':
                console.log('Moving west');
                break;
            default:
                // This causes a compile-time error if a new direction is added
                // and this case doesn't properly handle the new type, ensuring exhaustiveness.
                const invalidDirection: never = direction;
                throw new Error(`Invalid direction ${invalidDirection}`);
        }
    }
    

Key Differences Between unknown and never

  • Purpose:

    • The unknown type is used to indicate that a value exists but its exact type is unknown. It requires type checks or assertions before you can work with its value.
    • The never type is used to represent values that will never occur. It is primarily used with functions that never terminate normally to ensure type safety and completeness in control flows.
  • Assignments:

    • You can assign any value to a variable of type unknown.
    • You cannot assign any value, including null or undefined, to a variable of type never.
  • Operations:

    • Operations on an unknown variable are restricted; you must narrow down the type via type checks or assertions before performing operations.
    • An expression of type never can never be reached, and thus operations on never types are not meaningful unless in the context of exhaustiveness checking.

Practical Applications and Benefits

  1. Enhanced Type Safety:

    • By using unknown instead of any, developers can write safer and more predictable code. It encourages explicit type checking, reducing the risk of runtime type-related bugs.
  2. Robust Error Handling:

    • The never type helps in writing robust error handling mechanisms. When used with exhaustive switch-case statements, TypeScript ensures that all cases are accounted for, preventing runtime errors.
  3. Clarity and Maintainability:

    • Both unknown and never make your code more expressive by indicating where uncertainty exists (unknown) and where impossibility is assumed (never). This clarity improves maintainability and makes it easier for other developers to understand your code.
  4. Advanced Control Flow Analysis:

    • The never type plays a vital role in advanced TypeScript features like discriminated unions and exhaustiveness checks, allowing for deeper and more rigorous code analysis.

In summary, the unknown and never types in TypeScript address two different aspects of type safety. unknown is a versatile type for variables whose exact type is uncertain and requires explicit narrowing. On the other hand, never is specifically designed for situations where a value is logically impossible, primarily used for functions that never return or in exhaustive type checks. Together, these types enhance the overall type safety and reliability of TypeScript codebases, making them invaluable tools for modern JavaScript development.




Understanding TypeScript's unknown and never Types: A Step-by-Step Guide

TypeScript, an object-oriented programming language, enhances JavaScript by providing static typing, interfaces, classes, and more. Two frequently encountered types in TypeScript are unknown and never. Understanding these types thoroughly is essential for handling data with confidence and safety.

Objective: To walk through setting up a simple TypeScript application, manipulating data with unknown and never types, and tracing the data flow step-by-step.

Step 1: Setting Up Your TypeScript Environment

  1. Install Node.js

    • Node.js is required to run TypeScript and manage package dependencies. Download and install it from nodejs.org.
  2. Install TypeScript

    • Open your terminal (Command Prompt on Windows, Terminal on Unix or macOS).
    • Run the following command to install TypeScript globally using npm (Node Package Manager):
      npm install -g typescript
      
  3. Create a New TypeScript Project

    • Create a new directory for your project and navigate into it:
      mkdir ts-types-example
      cd ts-types-example
      
    • Initialize a new Node.js project:
      npm init -y
      
    • Create a tsconfig.json file to configure TypeScript:
      tsc --init
      
    • Modify the tsconfig.json to include relevant settings. For simplicity, you can keep the default settings and just focus on the root directory for our source files:
      {
        "compilerOptions": {
          "target": "ES6",
          "module": "CommonJS",
          "strict": true,
          "esModuleInterop": true,
          "skipLibCheck": true,
          "forceConsistentCasingInFileNames": true,
          "outDir": "./dist"
        },
        "include": ["src"]
      }
      
  4. Create a Source Directory

    • Create a src directory where TypeScript files will reside:
      mkdir src
      

Step 2: Writing TypeScript Code with unknown and never Types

  1. Create a TypeScript File

    • Inside the src directory, create a new file named example.ts:
      touch src/example.ts
      
  2. Implementing the Example Code

    • Open example.ts in your preferred text editor and implement the following TypeScript code:

      // Type: unknown
      // When you don't know the type of data you're receiving
      function processInput(input: unknown) {
        if (typeof input === 'string') {
          console.log(`String received: ${input}`);
        } else if (typeof input === 'number') {
          console.log(`Number received: ${input}`);
        } else if (typeof input === 'boolean') {
          console.log(`Boolean received: ${input}`);
        } else {
          console.log('Unknown type received');
        }
      }
      
      // Type: never
      // Represents values that never occur; function returns never if it always throws an exception or never finishes
      function generateError(errorMessage: string, errorCode: number): never {
        throw new Error(`${errorMessage} (${errorCode})`);
      }
      
      // Simulate receiving data and processing
      const userInput1: unknown = 'Hello TypeScript';
      processInput(userInput1); // Output: String received: Hello TypeScript
      
      const userInput2: unknown = 123;
      processInput(userInput2); // Output: Number received: 123
      
      const userInput3: unknown = true;
      processInput(userInput3); // Output: Boolean received: true
      
      const userInput4: unknown = {};
      processInput(userInput4); // Output: Unknown type received
      
      // Simulate an error condition
      try {
        generateError('Something went wrong', 500);
      } catch (error) {
        console.log(error.message); // Output: Something went wrong (500)
      }
      

Step 3: Compiling TypeScript to JavaScript

  • Use the TypeScript compiler (tsc) to compile your TypeScript code to JavaScript:
    tsc
    
  • This command compiles all TypeScript files in your src directory (as specified in tsconfig.json) to the dist directory.

Step 4: Running the Application

  • Navigate to the dist directory where the compiled JavaScript code resides:
    cd dist
    
  • Run the compiled JavaScript file using Node.js:
    node example.js
    

Expected Output:

String received: Hello TypeScript
Number received: 123
Boolean received: true
Unknown type received
Something went wrong (500)

Step 5: Tracing the Data Flow and Type Handling

  1. Receiving Input with unknown:

    • The processInput function accepts a parameter of type unknown. This means it can receive any type of data.
    • Inside the function, we use typeof to check the type of the received input and process it accordingly.
    • The unknown type forces us to perform type checking before performing any operations on the data. This is crucial for avoiding runtime errors and maintaining type safety.
  2. Handling Errors with never:

    • The generateError function demonstrates the never type. It represents operations that never complete normally.
    • The function throws an error, ensuring that any code following the function call is unreachable and indicating that the function will never return a value.
    • This is useful for handling error scenarios explicitly, improving the robustness of your application.

Conclusion

Understanding TypeScript's unknown and never types is vital for leveraging full type-checking capabilities in your applications, ensuring safer and more reliable code. By following this step-by-step guide, you now have a foundational understanding of how these types can be used in real-world TypeScript applications.

Feel free to experiment further by modifying the example.ts file, adding more types, and observing the results. Mastering TypeScript's type system will undoubtedly improve your programming skills and help you build more scalable and maintainable applications.




Top 10 Questions and Answers on TypeScript: The unknown and never Types

1. What is the unknown type in TypeScript, and how does it differ from the any type?

The unknown type in TypeScript represents a value that could be of any type, just like the any type. However, unlike any, unknown demands that you perform some sort of checking or assertion before you interact with the value. Essentially, unknown is a safer alternative to any that encourages type safety. Here's an example:

let exampleAny: any = "hello";
let exampleUnknown: unknown = "hello";

// With `any`, we can access properties without type checks:
exampleAny.toUpperCase();

// With `unknown`, TypeScript forces you to do a type check or assertion:
// Since `exampleUnknown` could be any type, TypeScript won't allow you to call `toUpperCase` directly:
// exampleUnknown.toUpperCase(); // Error: Object is of type 'unknown'.

// Instead, we must do a type guard to check if the value is a string:
if (typeof exampleUnknown === "string") {
    exampleUnknown.toUpperCase(); // Now this works because TypeScript knows the type.
}

Difference:

  • any: You can perform any operation on any — TypeScript trusts that you know what you're doing and disables type checking.
  • unknown: TypeScript makes you prove that you understand the underlying type. It works as a gatekeeper before allowing operations, enhancing type safety.

2. When should you use the unknown type in TypeScript?

Use the unknown type in scenarios where you may receive data of an uncertain type, and you want to enforce type safety. Here are some common cases:

  • Dynamic Type Checking: When working with data from external sources (e.g., user input, API responses).
  • ** narrowed types:** To perform safe type assertions or type guards.
  • Type Safety in Functions: When designing functions that can accept and handle parameters of unknown types.

Example:

function processInput(input: unknown): void {
    if (typeof input === "string") {
        console.log(input.toUpperCase());
    } else if (typeof input === "number") {
        console.log(input * 2);
    } else {
        console.log("Input type not supported");
    }
}

3. Can unknown be extended or implemented by other types?

You cannot directly extend or implement the unknown type because TypeScript requires explicit type checks to determine the nature of a value of type unknown. Instead, you can assign a value of a more specific type to an unknown type variable, and vice versa using type assertions or type guards.

let unknownValue: unknown;
unknownValue = "hello"; // This works because a string is a specific type.
unknownValue = 42;      // This works because a number is a specific type.

let stringValue: string = unknownValue;       // Error: Type 'unknown' is not assignable to type 'string'.
let narrowedString: string = unknownValue as string; // Works with explicit assertion.

4. What does the never type signify in TypeScript?

The never type represents values that never occur — typically used in cases where a function will always throw an error, prove to be an infinite loop, or never return normally. It's the type of functions that never return a value.

Examples:

  • Functions that throw errors:
function alwaysThrows(): never {
    throw new Error("This function always throws an error");
}
  • Infinite loops:
function infiniteLoop(): never {
    while (true) {
        // Do something continuously.
    }
}
  • Functions with unreachable statements:
function unreachable(): never {
    return "This will cause a TypeScript error"; // Error: Type 'string' is not assignable to type 'never'.
}

5. What's the primary use of the never type in TypeScript?

The primary use of the never type includes:

  • Function Signatures: It specifies that a function will never return normally. This is useful in functions that throw errors or contain infinite loops. It's a stronger guarantee of function behavior compared to using void.
  • Type Theory: In type-checking, it signifies values that are unreachable parts of a codepath or type definitions.
  • Union and Intersection Types: It can be used to exclude certain types. For instance, intersecting any type with never results in never, making it useful for advanced type manipulations.

Example:

// Using `never` to eliminate illegal states:
type PossibleStates = "loading" | "success" | "error";

function assertNever(value: never): never {
    throw new Error(`Unhandled value: ${value}`);
}

function handleState(state: PossibleStates): void {
    switch (state) {
        case "loading":
            console.log("Loading...");
            break;
        case "success":
            console.log("Success!");
            break;
        case "error":
            console.log("Error occurred.");
            break;
        default:
            assertNever(state); // Ensures no other states are possible.
            break;
    }
}

6. Can you assign a value of type never to other types in TypeScript?

Yes, you can assign a value of type never to other types in TypeScript because a never type can never exist. Therefore, it's safe to assign never to any other type since it will never produce a value that could violate type safety.

let anyValue: any = neverFunction();
let unknownValue: unknown = neverFunction();
let stringValue: string = neverFunction();
let numberValue: number = neverFunction();

function neverFunction(): never {
    throw new Error("This function never returns");
}

Explanation:

  • Since neverFunction will always throw an error, anyValue, unknownValue, stringValue, and numberValue won't actually be assigned a value at runtime, thus maintaining type safety.

7. Is it possible to use the never type in conditional types?

Yes, you can use the never type in conditional types to perform advanced type manipulations. The never type can help exclude certain branches of conditional logic or enforce stricter type checking.

Example:

type CheckNever<T> = T extends never ? "Never" : "Not Never";

type Result1 = CheckNever<never>; // Result1 is "Never"
type Result2 = CheckNever<string>; // Result2 is "Not Never"

Advanced Usage:

Combining never with utility types and type inference, you can enforce stricter type constraints.

type RemoveNever<T> = T extends never ? never : T;

type ExampleType = string | number | never; // Equivalent to string | number
type CleanedType = RemoveNever<ExampleType>; // CleanedType is string | number

8. How should you use the unknown and never types together in TypeScript?

Using unknown and never together can enhance type safety and expressiveness in complex type definitions. unknown is useful for dealing with uncertain types, while never can be used to model impossible or invalid states.

Example:

function processResponse(response: unknown): string {
    if (typeof response === "string") {
        return response;
    } else if (typeof response === "object" && response !== null && "message" in response) {
        return response.message;
    } else {
        throw new Error("Unexpected response format");
    }
}

function handleError(error: unknown): never {
    if (error instanceof Error) {
        console.error(`Error caught: ${error.message}`);
    }
    throw error; // Rethrow the error if it's not an Error instance.
}

try {
    const response = getApiData();
    const result = processResponse(response);
    console.log(result);
} catch (error) {
    handleError(error);
}

Explanation:

  • processResponse: Utilizes unknown to handle responses of uncertain types safely, validating and narrowing down the type using type guards.
  • handleError: Uses never to model that the function never returns normally, making it clear that an error is always thrown.

9. What are the common pitfalls to avoid when working with unknown and never types in TypeScript?

While unknown and never are powerful tools, they can lead to pitfalls if not used carefully. Here are some common issues to avoid:

  • Overusing unknown: While unknown is safer than any, overusing it can lead to excessive type assertions and Type Guards, complicating code.

    // Example of poor `unknown` usage:
    function poorProcess(input: unknown): string {
        return (input as string).toUpperCase(); // Bad practice: Over-reliance on type assertion.
    }
    
  • Incorrect Union with never: Merging any type with never results in never, which might be unintended.

    type InvalidType = string | never; // InvalidType is `never`, which might not be the desired outcome.
    
  • Misusing never in Conditional Types: Accidentally using never in conditional types can lead to unexpected results, such as stripping away all types.

    type StrangeType<T> = T extends any ? never : T;
    // StrangeType<string> becomes `never` instead of `never`.
    
  • Ignoring never Functions: Ignoring the return value of a never function can lead to runtime errors if the function is expected to return a value.

    function alwaysDies(): never {
        throw new Error("Dies every time");
    }
    
    const result: string = alwaysDies(); // Unsafe: Assumes `alwaysDies` returns a string.
    

Best Practices:

  • Reasonably Use unknown: Apply unknown only when dealing with truly uncertain types.
  • Verify never Usage: Ensure that never is used to represent truly unreachable or invalid states.
  • Avoid Direct Assignments: Be cautious when merging types with never in complex type definitions.

10. What are the benefits of using the unknown and never types compared to alternatives like any or implicit types?

Using unknown and never offers several benefits over alternatives like any or implicit types, enhancing type safety and code reliability:

  • Enhanced Type Safety:

    • unknown: Encourages type checking and assertions, reducing runtime errors.
      let anyValue: any = "hello";
      anyValue.toUpperCase(); // No TypeScript error, but can cause runtime issues.
      
      let unknownValue: unknown = "hello";
      unknownValue.toUpperCase(); // TypeScript error requires type checking.
      if (typeof unknownValue === "string") {
          unknownValue.toUpperCase(); // Now safe.
      }
      
    • never: Helps catch impossible states and invalid code branches during compilation.
      function unreachableFunction(): never {
          return "This will cause a TypeScript error"; // Error: Type 'string' is not assignable to type 'never'.
      }
      
  • Improved Code Clarity:

    • unknown: Makes the code intent clearer by signaling that a variable's type is uncertain and needs handling.
      function processUnknown(input: unknown): void {
          // Clear intent to handle uncertain data.
          if (typeof input === "string") { /* ... */ }
      }
      
    • never: Documents that a function will never return normally, improving code understanding.
      function throwAlways(): never {
          throw new Error("Function always throws");
      }
      
  • Better Type Checking:

    • unknown: Prevents unsafe operations and ensures type-safe interactions through assertions and guards.
    • never: Helps eliminate invalid states and unreachable code, making type definitions more robust.
  • Advanced Type Manipulations:

    • Using unknown and never in advanced type definitions allows for more nuanced and flexible type systems.

Conclusion: The unknown and never types are powerful additions to TypeScript's type system, promoting stricter typing, safer code, and clearer intent. While unknown helps manage uncertainty, never ensures that certain code paths are impossible, making these types invaluable tools for developing robust applications. By understanding and leveraging these types effectively, developers can write more reliable and maintainable code.