
Stop Using 'as' in TypeScript: Why Type Assertion Is Dangerous
Using 'as' to silence errors? It's lying to the compiler. Learn why Type Assertions cause runtime crashes and how to use Type Guards instead.

Using 'as' to silence errors? It's lying to the compiler. Learn why Type Assertions cause runtime crashes and how to use Type Guards instead.
A deep dive into Robert C. Martin's Clean Architecture. Learn how to decouple your business logic from frameworks, databases, and UI using Entities, Use Cases, and the Dependency Rule. Includes Screaming Architecture and Testing strategies.

Connecting incompatible interfaces. How to make old code work with new code without rewriting.

SRP, OCP, LSP, ISP, DIP. The foundation of maintainable software architecture. Examples in TypeScript.

Code is read 10x more than it is written. Why meaningful names matter, how to split functions, and why comments are often failures. The art of refactoring.

Too lazy to define API response types? Or "Sure the data matches"?
// I'm 100% sure server sends a User
const user = response.data as User;
console.log(user.address.city);
VSCode red lines vanished. Build succeeded. Deployment happened. And Sentry alerts exploded.
Uncaught TypeError: Cannot read properties of undefined (reading 'city')
Turns out, the server sent null for address in edge cases.
But because I claimed "It IS a User!" with as User, TypeScript gave up checking and let the bomb pass.
I thought as was like Casting in Java/C.
I thought it actually transformed the data structure.
But as is Type Assertion.
It means "Trust me, I know better than you." It's lying to the compiler.
It does NOTHING at runtime.
In compiled JS, as disappears, leaving raw, unchecked data.
I visualized it as "Blindfolding Airport Security."
The officer can't see the bag. So you Pass (Compile Success). But preventing the bomb from exploding on the plane (Runtime Error) is impossible.
as is a Mute Button for warnings, not a Fix.
as is a silence button, not a fix. Use Type Guards (if, is, Zod) to prove safety to the compiler.
unknown vs anyWhen refactoring to remove as, you will encounter any and unknown.
It is crucial to understand the difference.
any: "I don't care." It turns off type checking completely. any spreads like a virus. If one variable is any, everything it touches becomes any.unknown: "I don't know YET." It is safe. You cannot use a variable of type unknown until you narrower its type (using Type Guards).Refactoring Strategy:
If you have const data: any, change it to const data: unknown.
The compiler will immediately scream at every usage of data.
"Object is of type 'unknown'."
This is good! It forces you to write validation logic (Type Guards) for each usage.
// bad
const data: any = JSON.parse(str);
console.log(data.x.y.z); // Crash? Who knows.
// good
const data: unknown = JSON.parse(str);
// console.log(data.x); // Error: Object is 'unknown'.
if (isMyData(data)) {
console.log(data.x); // Safe!
}
Type assertions (as) are technically a "feature" of TypeScript, but in a modern codebase, they should be treated as a "code smell."
They defeat the very purpose of using TypeScript: Static Analysis.
If you find yourself writing as User, ask yourself:
A strict "No-Assertion" policy might feel restrictive at first, but it eliminates an entire class of "undefined is not a function" errors from production. Trust the Red Line. It's there to save you.
To replace as, use Type Guards.
Prove to the compiler, "Look, it really is safe."
in or typeoffunction printCity(user: unknown) {
// ❌ Bad: Just assuming
// console.log((user as User).address.city);
// ✅ Good: Proving
if (typeof user === 'object' && user !== null && 'address' in user) {
// TS knows 'user' has 'address' property inside this block
// ... logic
}
}
is)The most powerful weapon. Encapsulate logic in a function.
interface User {
name: string;
address: { city: string };
}
// If this returns true, TS treats 'target' as 'User'
function isUser(target: unknown): target is User {
return (
typeof target === 'object' &&
target !== null &&
'address' in target
);
}
// Usage
const response = await fetch('/api/user');
const data = await response.json();
if (isUser(data)) {
console.log(data.address.city); // Safe! Auto-complete works!
} else {
console.error("Invalid Data Format", data); // Handle error elegantly
}
Now, instead of crashing, your app handles bad server data gracefully.
Writing isUser manually is tedious.
Use Zod schema validation.
import { z } from 'zod';
// Runtime Schema
const UserSchema = z.object({
name: z.string(),
address: z.object({
city: z.string()
})
});
// Auto-Type Inference
type User = z.infer<typeof UserSchema>;
// Validate
const result = UserSchema.safeParse(response.data);
if (result.success) {
console.log(result.data.address.city); // 100% Safe
} else {
console.error(result.error); // Tells exactly what's wrong
}
Zod ensures the runtime value perfectly matches the TypeScript type.
I stopped using as for APIs. I only use Zod.
asThere is one exception. "When I definitely know better than the compiler." Usually for DOM Elements or Constants.
// Start of app, I assume #app exists
const root = document.getElementById('app') as HTMLElement;
satisfies Operator (TS 4.9+)TypeScript 4.9 introduced satisfies, the holy grail replacement for as in many cases.
It enforces strict type checking while preserving specific type inference.
as vs : Type vs satisfies// 1. Type Annotation (Widens type)
const palette: Record<string, string | number[]> = {
red: [255, 0, 0],
green: "#00ff00",
};
// ❌ Error! TS doesn't know if palette.red is string or array.
// palette.red.map(...) // Fail
// 2. as (Lying)
const palette2 = {
red: [255, 0, 0],
green: "#00ff00",
} as Record<string, string | number[]>;
// ❌ Risky.
// 3. satisfies (Perfect)
const palette3 = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | number[]>;
// ✅ Success!
// TS checks compatibility BUT remembers that 'red' is exactly 'number[]'.
palette3.red.map(x => x * 2);
palette3.green.toUpperCase();
Use satisfies for config objects or themes. Stop using as const combined with explicit types awkwardly.
In an e-commerce app, both UserId and OrderId were string.
We kept swapping arguments by mistake.
function cancelOrder(userId: string, orderId: string) { ... }
// Argument swap mistake
cancelOrder(orderId, userId); // TS says OK because both are strings!
To optimize this, we used Branded Types (Nominal Typing).
// Fake strict types using intersection
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };
// Factory function (Type Guard)
function createUserId(id: string): UserId {
return id as UserId; // The ONLY place 'as' is allowed!
}
const myUser = createUserId("user_123");
// cancelOrder(myOrder, myUser); // 🚨 Compile Error! Type Mismatch.
We blocked logical errors at compile time.
as is only acceptable effectively when hiding implementation details inside a utility function like this.
! (Non-null Assertion) bad too?Yes. user!.name is just user as NonNullableUser.
It screams "This is never null!" to the compiler.
But code changes. It might become null later.
Use ?. (Optional Chaining) or if guards instead.