Common Runtime Type Mistakes in TypeScript
TypeScript types disappear at runtime, and the most common mistakes happen when developers forget this. Learn the pitfalls of typeof, instanceof, type assertions, and structural checking so you stop trusting types at runtime.
TypeScript gives you a powerful type system, but every type annotation, interface, and generic is stripped away when your code compiles to JavaScript. The runtime sees none of it. The most common and dangerous bugs come from forgetting this fact and assuming types provide protection beyond compile time.
Here are the mistakes developers make most often, why they happen, and how to avoid them.
1. Trusting typeof Without Checking for null
The typeof operator in JavaScript has a well-known quirk: typeof null returns the string "object", not "null". TypeScript inherits this behavior because it does not change JavaScript runtime semantics.
function describeValue(value: unknown) {
if (typeof value === "object") {
console.log(value.toString());
}
}Calling this function with null compiles without error but throws at runtime, because null passes the typeof check and then crashes when the code calls a method on it.
The fix is to check for null explicitly alongside the typeof check.
function describeValue(value: unknown) {
if (value !== null && typeof value === "object") {
console.log(value.toString());
}
}Alternatively, use a truthiness check that handles both null and undefined in a single condition, at the cost of also filtering out other falsy values.
function describeValue(value: unknown) {
if (value && typeof value === "object") {
console.log(value.toString());
}
}The truthiness check filters out null, undefined, zero, an empty string, and NaN. Choose this only when excluding all falsy values is acceptable.
2. Using Type Assertions Instead of Validation
Type assertions like as User tell the compiler to trust you. They provide exactly zero runtime protection. If the data does not match the asserted type, the error surfaces later as an unpredictable crash.
interface User {
id: number;
name: string;
}
const raw = '{"id": 1}';
const user = JSON.parse(raw) as User;
console.log(user.name.toUpperCase());This compiles. TypeScript trusts the assertion. At runtime, the name property is undefined, and calling a string method on it throws a TypeError.
The fix is to validate instead of asserting, starting with a guard that checks the shape at runtime.
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string"
);
}The guard runs before the value is trusted, so a malformed object is caught immediately instead of crashing deep inside unrelated code later.
const parsed: unknown = JSON.parse(raw);
if (isUser(parsed)) {
console.log(parsed.name.toUpperCase());
} else {
console.error("Invalid user data");
}Validation catches the missing name field at the point where the data enters the system, not when it is used somewhere deep in the code. For more on this, see runtime validation in TypeScript.
3. Assuming Types Exist at Runtime
TypeScript interfaces, type aliases, and generics are purely compile-time constructs. You cannot use them in runtime logic.
interface Dog {
breed: string;
}There is no Dog at runtime. You cannot write an instanceof check against it, because the interface does not exist in the emitted JavaScript. The compiler reports an error on this pattern.
The same applies to generics.
function isArrayOf<T>(value: unknown): value is T[] {
return Array.isArray(value);
}This compiles, but the type predicate value is T[] is misleading. The function only checks that the value is an array.
It does not and cannot check that the elements match the generic type, because that type parameter does not exist at runtime. A guard meant to check for an array of strings will happily accept an array of numbers.
The fix is to accept a runtime validation function for the element type.
function isArrayOf<T>(
value: unknown,
elementGuard: (el: unknown) => el is T
): value is T[] {
return Array.isArray(value) && value.every(elementGuard);
}Now the element validation runs at runtime and the result is trustworthy.
4. Confusing Structural Typing with Nominal Identity
TypeScript uses structural typing. Two types with the same shape are considered the same type, even if they have different names and represent different concepts.
type UserId = { value: string };
type OrderId = { value: string };
function fetchUser(id: UserId) {
console.log(id.value);
}
const orderId: OrderId = { value: "ord-123" };
fetchUser(orderId);This compiles without error because both types have identical structures. The type names are different, but the compiler only looks at the shape, so one is assignable anywhere the other is expected.
To make these types distinct at compile time, use a branded type that attaches a fake marker property only the type checker sees.
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function makeUserId(value: string): UserId {
return value as UserId;
}
function makeOrderId(value: string): OrderId {
return value as OrderId;
}Passing one branded id where the other is expected now fails to compile, even though both are strings underneath at runtime, because the brand makes the two types incompatible.
function fetchUser(id: UserId) {
console.log(id);
}
const orderId = makeOrderId("ord-123");
fetchUser(orderId);The compiler now reports an error because the two branded types are not assignable to each other. The hidden brand property never exists at runtime, but the compiler treats the types as distinct.
5. Forgetting That Enums Exist at Runtime
Regular TypeScript enums generate JavaScript objects at runtime. This is different from most other TypeScript constructs, which are erased.
enum Status {
Active,
Inactive,
}This compiles to JavaScript that creates a Status object with bidirectional mappings: the Active member is 0, and looking up 0 on the object gives back the string "Active". The enum is real at runtime.
var Status;
(function (Status) {
Status[Status["Active"] = 0] = "Active";
Status[Status["Inactive"] = 1] = "Inactive";
})(Status || (Status = {}));If you import this enum and pass it across a network boundary, the receiving side needs the same enum definition. Use a const enum when you want the enum values inlined and erased at compile time.
const enum Status {
Active,
Inactive,
}
console.log(Status.Active);A const enum compiles away entirely, leaving just the plain number in its place. The enum object itself does not exist at runtime. Choose a const enum for internal use where runtime access is not needed, and a regular enum when you need to iterate over values or use them as runtime keys.
6. Misusing instanceof Across Execution Contexts
The instanceof operator checks the prototype chain, which can break across different JavaScript execution contexts like iframes or different Node.js vm contexts.
function isError(value: unknown): value is Error {
return value instanceof Error;
}If the error was created in a different realm, for example an iframe or a vm module, the instanceof check returns false even though the value is structurally an Error. This is rare in most applications but common in test frameworks, browser extensions, and serverless environments.
A more robust check uses duck typing.
function isError(value: unknown): value is Error {
return (
typeof value === "object" &&
value !== null &&
"message" in value &&
"name" in value
);
}This checks for the properties an Error should have rather than relying on the prototype chain. It works across execution contexts.
For more on handling caught errors safely, see unknown errors in TypeScript. For structured validation approaches, see validate API data in TypeScript.
Rune AI
Key Insights
- TypeScript types are erased at compile time and do not exist in the emitted JavaScript.
- typeof null returns 'object', so always combine typeof checks with explicit null checks.
- as Type assertions do nothing at runtime and will not catch mismatched data.
- Two types with the same structure are interchangeable even with different names.
- Enums exist at runtime; const enums do not. Know which you are using.
Frequently Asked Questions
Why does typeof null return 'object' in TypeScript?
Is as any different from as unknown as Type?
Do TypeScript enums exist at runtime?
Conclusion
The most dangerous runtime mistakes happen when developers assume TypeScript types protect them beyond compile time. Types are erased. typeof has JavaScript quirks. Type assertions are compile-time lies. Structurally identical types can come from entirely different sources. The fix is simple: validate data at the boundary, check for null explicitly, and never trust an as assertion to protect you at runtime.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.