Safeguard
Developer Tools•

Why is a TypeScript type assertion not input validation?

A TypeScript assertion tells the compiler a value has a type but checks nothing at runtime. What to flag in review and how to validate untrusted JSON.

Safeguard Security Team
4 min read

A TypeScript type assertion such as as User tells the compiler to trust you. It is removed at compile time and performs no check when the code runs. Applied to request bodies or parsed JSON, it makes unvalidated data look validated, so flag it in review.

What the language says

The TypeScript handbook describes type assertions as a way to specify a more specific type when you have information the compiler cannot know. Its example narrows a generic element to a canvas element. It then states that, like a type annotation, assertions are removed by the compiler and do not affect runtime behaviour. Because they are removed at compile time, there is no runtime checking associated with a type assertion.

For a canvas element you know exists on your own page, that is fine. For data that arrived over the network, it is a trap.

How it goes wrong

app.post("/transfer", (req, res) => {
  const body = req.body as TransferRequest;
  moveMoney(body.fromAccount, body.toAccount, body.amount);
});

This looks typed. Editor hints work. But if the client sends a string for amount, an array for toAccount, or omits a field entirely, nothing stops it. The compiler believed you. The runtime never asked.

The same applies to JSON.parse(text) as Config, to values read from a database column typed loosely, and to anything from fetch followed by .json().

What to flag in review

PatternQuestion to ask
req.body as SomethingWhere is the runtime check?
JSON.parse(x) as SomethingWho controls x?
as unknown as SomethingWhy is the compiler being bypassed?
any on a function parameter that receives outside dataWhat narrows it?
Non-null assertion on a value from a requestWhat if it is missing?

An assertion is not wrong in itself. The review question is always whether the data came from a trusted place and, if not, where the check is.

The fix: validate, then derive the type

Validate at the boundary with a runtime schema, and let the type come from the schema or from a type guard that actually checks:

function isTransfer(x: unknown): x is TransferRequest {
  const t = x as Record<string, unknown>;
  return typeof t?.fromAccount === "string"
      && typeof t?.toAccount === "string"
      && typeof t?.amount === "number"
      && Number.isFinite(t.amount) && t.amount > 0;
}

app.post("/transfer", (req, res) => {
  if (!isTransfer(req.body)) return res.status(400).end();
  moveMoney(req.body.fromAccount, req.body.toAccount, req.body.amount);
});

Schema libraries can do this more compactly, and they can generate the type so the two never drift. The choice of library matters less than the rule: no outside data reaches business logic without a runtime check.

This lines up with the OWASP Secure Code Review Cheat Sheet, which lists server side validation, allowlist validation and length limits in its input validation checklist. Compile time types satisfy none of those.

A small review habit

Search the diff for the word as followed by a type name, and for any. Each hit is a one second question: does this value come from outside, and was it checked?

Quick check

Does strict mode catch this?

No. Strict mode checks consistency inside the type system. It cannot know what shape a request will have at runtime.

Are type guards enough?

Yes if they check everything the code relies on, including ranges and lengths. A guard that checks the field names alone leaves values unchecked.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.