TypeScript's most dangerous keyword is two letters long.
as does not convert anything. It does not check anything. It tells the compiler "trust me", and the compiler does. If you are wrong, the error moves from your editor to your users.
Here are five places I keep finding it in real codebases, each one compiling cleanly and each one a crash waiting for the right input.
1. Casting an API Response
const res = await fetch('/api/users/1');
const user = (await res.json()) as User;
console.log(user.profile.avatarUrl);
res.json() returns Promise<any>. Casting any to User is the compiler equivalent of a handshake agreement. If the backend renames a field, makes profile optional, or returns an error object with a 200 status, this line still compiles and the crash happens three functions later with a message that has nothing to do with the cause.
What to write instead: validate the data at the boundary.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
profile: z.object({ avatarUrl: z.string().url().nullable() }),
});
type User = z.infer<typeof UserSchema>;
const user = UserSchema.parse(await res.json());
Now the type is derived from a check that actually ran. If the shape is wrong, you get a clear error at the exact line where bad data entered your app.
2. The Non-Null Assertion on Environment Variables
const client = new MongoClient(process.env.DATABASE_URL!);
process.env.X is typed string | undefined, which is honest. The ! deletes the undefined from the type and nothing else. When the variable is missing in a new environment, you get a confusing failure from deep inside a library instead of "DATABASE_URL is missing".
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
}
const client = new MongoClient(requireEnv('DATABASE_URL'));
The app now fails at startup, with the variable name in the message. That is the cheapest place to find out.
3. The Non-Null Assertion on find()
const user = users.find((u) => u.id === id)!;
sendEmail(user.email);
find returns undefined when nothing matches, and "nothing matches" is exactly the case your tests forgot. The ! makes the compiler stop warning you about the one scenario worth warning about.
const user = users.find((u) => u.id === id);
if (!user) throw new Error(`User ${id} not found`);
sendEmail(user.email);
After the check, TypeScript narrows user to a real object on its own. No assertion required.
4. Casting Object.keys
type Settings = { theme: string; lang: string };
function logSettings(s: Settings) {
(Object.keys(s) as (keyof Settings)[]).forEach((k) => {
console.log(s[k].toUpperCase());
});
}
const fromApi = { theme: 'dark', lang: 'en', retries: 3 };
logSettings(fromApi);
This compiles. TypeScript types are structural, so an object with extra properties is still a valid Settings when it arrives through a variable. Object.keys returns plain string[] for exactly this reason, and the cast to keyof Settings overrides that protection. At runtime retries is a number, and toUpperCase is not a function.
const SETTING_KEYS = ['theme', 'lang'] as const;
function logSettings(s: Settings) {
SETTING_KEYS.forEach((k) => console.log(s[k].toUpperCase()));
}
Iterate over a list of keys you control, not over whatever the object happens to contain.
5. Casting the Caught Error
try {
await saveProfile(data);
} catch (e) {
toast((e as Error).message);
}
In JavaScript you can throw anything: a string, a number, undefined, a plain object. Libraries do it. With strict on, TypeScript types e as unknown to keep you honest, and as Error is the line that undoes it. If something throws a string, .message is undefined and your toast says "undefined".
catch (e) {
const message = e instanceof Error ? e.message : 'Something went wrong';
toast(message);
}
A Bonus Lie That Lives in Your Config
const items: Item[] = [];
const first = items[0];
first.name; // typed as Item, crashes at runtime
Array index access is typed as Item, not Item | undefined, because the compiler cannot know the length. One line in tsconfig.json fixes it:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
This flag is not part of strict, so most projects never turn it on. It adds a few extra checks, and every one of them is a real case where the array could have been empty.
The Rule
A cast is a promise you make to the compiler that it cannot verify. Data from outside your program needs a runtime check, not a type assertion.
An easy audit: search your project for as and for !. and !;. For each hit, ask what happens when the assumption is false. Casts on your own literals are usually fine. Casts on network responses, environment variables, search results and caught errors are where the bugs live.
I keep these checks in the shared lib/ folder of the TypeScript-first templates on pixelanas.com, so a missing environment variable or a changed API shape fails loudly on the first request instead of silently in production.
Get the templates: https://pixelanas.gumroad.com
How many as casts does your project have, and how many of them are on data you do not control? Drop your count in the comments.
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)