TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
11: The any, unknown, and never Types
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
errorResponseparameter fromanytounknown. - Implement a type check to ensure
errorResponseis an object and contains amessagestring before logging it. - The
ErrorCodeunion contains'INVALID_CARD','INSUFFICIENT_FUNDS', and'GATEWAY_TIMEOUT'. Use anevertype 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;
}
}There are no comments for now.