TypeScript catches the stupid bugs before they reach production. It slaps your wrist for the misspelled property, the argument you forgot, and the branch that returns the wrong shape. You fix it, the build passes, and you ship. But static types have a hard limit. Once the compiler finishes, every annotation is stripped away. The JavaScript engine running your code has never heard of your interfaces, your branded types, or your carefully constrained string literals. It knows only values and the actual rules of the language.

Build passing does not mean runtime safe. Tests green does not mean users see a stable application. If your mental model stops at the TypeScript boundary, you are flying blind in the exact place where crashes actually happen.

The Compile-Time Mirage

TypeScript’s entire type system is erased during compilation. Open the compiled JavaScript output for any project and you will find no trace of interface, type, or generic constraints. They are design-time scaffolding. The browser or Node.js process executes plain JavaScript, and the values flowing through your functions are not guaranteed to match the types you declared on paper.

This gap matters most at the edges of your system. Network responses, user input, and third-party libraries can inject values that violate your types. A variable you declared as strictEmail: string can still hold a number at runtime if bad data enters through an untrusted API. TypeScript cannot follow your code into production to enforce anything. The runtime operates on a completely separate plane, and conflating the two planes leads to failures that static analysis will never catch.

When Numbers Betray You

TypeScript sees a number. The JavaScript engine sees an IEEE 754 double-precision float. That distinction is harmless until it is catastrophic.

JavaScript allocates 64 bits for every number, but only 53 bits store the mantissa. That creates a safe integer ceiling of 9,007,199,254,740,991. Anything larger gets rounded to the nearest representable value. In practice, two genuinely different identifiers can collapse into the same value inside your application.

Snowflake IDs and other distributed 64-bit integer identifiers routinely exceed that limit. Financial systems tracking high-value amounts in minor currency units can also brush against it. The danger often appears before your business logic even runs: JSON.parse eagerly converts numeric literals in a payload into JavaScript numbers, silently truncating precision on arrival. Your type definition might promise id: number, but the runtime value is already corrupted before the first function call.

The fix is straightforward but requires discipline across your stack. Keep large identifiers as strings during network transport. In your JSON schemas and API contracts, define these fields as strings, not numbers. If you must perform arithmetic on values beyond the safe range, reach for BigInt. Be careful, though: BigInt does not implicitly mix with standard JavaScript numbers, and JSON.stringify cannot serialize a BigInt without throwing unless you explicitly convert it back to string first. Treat IDs as opaque tokens by default. Only parse into numeric form when you are inside an isolated calculation module that truly needs