Skip to Content
Course content

11: The any, unknown, and never Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was auditing a codebase for a fintech client that had migrated from JavaScript to TypeScript in a huge rush. I found a core payment processing function where almost every variable was typed as any. The original developer had basically told the compiler, "Trust me, I know what I'm doing," to avoid fighting with complex API responses. It worked fine in development, until a third-party API changed a field from a string to null. Because the code used any, TypeScript stayed silent. The app crashed in production during a peak sales window because the code tried to call .trim() on a null value. That's the "any trap"—it gives you the illusion of type safety while actually disabling the very tool you installed TypeScript to use.

The Danger of the any Escape Hatch

When you use any, you're telling TypeScript to turn off type checking for that specific variable. You can call any method on it, assign it to any other type, and pass it into any function. It's the ultimate escape hatch, and while it's tempting when you're frustrated with a complex generic or a messy legacy library, it's a liability. If you find yourself reaching for any, you're essentially writing plain JavaScript again, but with more keystrokes.

// This is effectively "turning off" TypeScript
function handleResponse(data: any) {
    console.log(data.user.profile.email); // No compile error, but will crash if profile is missing
}

I generally tell my team that any should be a last resort, used only when you're migrating a massive file and don't have the time to map out every type immediately. But once the code is in, your goal should be to replace every any with something more descriptive.

Switching to unknown for Safer Boundaries

If you genuinely don't know what type a value will be—like data coming from an external API or a JSON.parse() call—you should use unknown instead of any. They seem similar because both can hold any value, but the difference is in how you use them. unknown is the "safe" version of any. It tells TypeScript: "This could be anything, so don't let me do anything with it until I prove what it actually is."

You can't call methods on an unknown value or assign it to a typed variable without first performing a type check (a type guard). This forces you to handle the edge cases that lead to those production crashes I mentioned earlier.

function processInput(input: unknown) {
    // console.log(input.toUpperCase()); // Error: Object is of type 'unknown'

    if (typeof input === 'string') {
        // Now TypeScript knows input is a string
        console.log(input.toUpperCase()); 
    } else if (typeof input === 'number') {
        console.log(input.toFixed(2));
    }
}

Using never to Catch the Impossible

Then we have never. This one is a bit more abstract. While unknown represents "any value," never represents "no value." You'll mostly see never in two scenarios: functions that throw errors (since they never actually return a value) or for "exhaustive checking" in switch statements.

Exhaustive checking is where never really shines for us as engineers. Imagine you have a union type for different user roles. If you add a new role to that union later, you want the compiler to scream at you if you forgot to handle that new role in your logic. By assigning the "default" case of a switch to a variable of type never, you create a compile-time guarantee that every possible case has been handled.

type Role = 'admin' | 'editor' | 'viewer';

function getPermissions(role: Role) {
    switch (role) {
        case 'admin': return ['all'];
        case 'editor': return ['edit', 'view'];
        case 'viewer': return ['view'];
        default:
            // This ensures that if 'Role' grows, this line will throw a type error
            const _exhaustiveCheck: never = role;
            return _exhaustiveCheck;
    }
}

If you were to add 'guest' to the Role type, the default block would suddenly fail to compile because role would be 'guest', and 'guest' cannot be assigned to never. It's a brilliant way to avoid the "I forgot to update the switch statement" bug.




📋 Practical Task

Refactoring the API Error Handler

You've inherited a piece of code that handles errors from a payment gateway. The current implementation uses any, which has led to several runtime crashes. Your goal is to make this code type-safe using the concepts from this lesson.

Requirements:

  • Change the errorResponse parameter from any to unknown.
  • Implement a type check to ensure errorResponse is an object and contains a message string before logging it.
  • The ErrorCode union contains 'INVALID_CARD', 'INSUFFICIENT_FUNDS', and 'GATEWAY_TIMEOUT'. Use a never type assignment in the default case of the switch statement to ensure all error codes are handled.
type ErrorCode = 'INVALID_CARD' | 'INSUFFICIENT_FUNDS' | 'GATEWAY_TIMEOUT';

interface ApiError {
    code: ErrorCode;
    message: string;
}

function handlePaymentError(errorResponse: any) {
    // TODO: Change 'any' to 'unknown' in the function signature
    
    // TODO: Use a type guard to safely check if errorResponse is an ApiError
    // (Hint: check if it's an object, not null, and has the required properties)

    switch (errorResponse.code) {
        case 'INVALID_CARD':
            console.log("Please check your card details.");
            break;
        case 'INSUFFICIENT_FUNDS':
            console.log("Your balance is too low.");
            break;
        case 'GATEWAY_TIMEOUT':
            console.log("The server is lagging, try again.");
            break;
        default:
            // TODO: Implement exhaustive check using the 'never' type
            break;
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.