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.

7 min read

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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

javascriptjavascript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Why does typeof null return 'object' in TypeScript?

This is inherited from JavaScript, where typeof null === 'object' is a historical bug that cannot be fixed without breaking the web. TypeScript does not correct this at runtime. Always add an explicit null check alongside typeof checks.

Is as any different from as unknown as Type?

Both bypass type checking, but as unknown as Type is slightly safer because the intermediate unknown step forces you to acknowledge you are doing something unsafe. Neither provides runtime protection.

Do TypeScript enums exist at runtime?

Yes, numeric enums generate JavaScript objects with reverse mappings. String enums generate simpler objects without reverse mappings. Both exist at runtime, unlike most other TypeScript constructs. Const enums are erased during compilation.

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.