A TypeScript secure coding checklist should cover five things: strict compiler settings, runtime validation of every external input, a ban on unchecked casts and dynamic code execution, guarded object merging, and controlled dependencies. Types protect you from your own mistakes. They do nothing against hostile data at runtime.
Why do types not make the code safe?
The TypeScript compiler erases types before the code runs. What ships is plain JavaScript. If a handler declares that req.body is a User, nothing checks that claim when a request arrives. The attacker sends whatever JSON they like, and the program behaves as if the shape were true.
That gap between declared and actual data is where most TypeScript security bugs live. JSON.parse returns any. So do many database drivers, HTTP clients and message queue libraries. Every any that reaches business logic is an unchecked claim.
What compiler settings should be mandatory?
Turn on strict in tsconfig.json. It enables null checks, strict function types and the rule against implicit any. Add noUncheckedIndexedAccess so array and map lookups can be undefined. Then enforce the settings in CI so a developer cannot loosen them locally and push.
Treat // @ts-ignore, // @ts-expect-error and the as keyword as review triggers. A cast from unknown to a concrete type, with no check in between, is the same as any with extra steps.
How should external input be handled?
Define a schema at each trust boundary: HTTP bodies, query strings, environment variables, webhook payloads, queue messages, and files. Parse with a schema validation library, reject on failure, and derive the static type from the schema so the two cannot drift. Validate on the server even if the client validates too.
Prefer allow lists to deny lists. State what a valid value looks like (length, character set, range) and refuse the rest. The OWASP input validation guidance makes the same point and adds that validation is not a replacement for output encoding or parameterized queries.
Which patterns need a hard stop?
eval,new Function, andsetTimeoutwith a string argument.child_process.execwith concatenated strings. UseexecFilewith an argument array.- SQL built by template literals. Use placeholders.
dangerouslySetInnerHTML,innerHTMLandv-htmlwith data you did not sanitize.- Recursive merge or clone helpers fed with user-controlled objects.
That last one is prototype pollution. If a merge function copies a key named __proto__ or constructor from parsed JSON, the attacker can change defaults for every object in the process. Use Object.create(null) for lookup maps, Map where keys are untrusted, and Object.hasOwn for membership tests. Reject those key names at the validation layer.
What about dependencies?
Most TypeScript services pull in hundreds of packages. Commit the lockfile, install in CI with the command that fails when the lockfile and manifest disagree, and review install scripts for anything new. Keep @types packages in step with the runtime package they describe, because a mismatch gives a false sense of what a function returns.
Also generate a software bill of materials on every build so you can answer "are we running this version?" in minutes. Safeguard can scan the dependency tree and use reachability analysis to show which advisories your code actually calls, which helps when the raw list is long.
What to do
- Enable
strictandnoUncheckedIndexedAccess; fail CI if they change. - Lint for
any, uncheckedascasts and the ignore comments, with a short allowed list. - Put a schema at every trust boundary and infer types from it.
- Ban dynamic code execution and string-built shell and SQL in lint rules.
- Reject
__proto__,constructorandprototypekeys in merged input. - Commit lockfiles, use a failing clean install in CI, and keep an SBOM per build.
- Add one test per boundary that sends the wrong shape and expects a 400.
Quick check
Does strict mode stop injection bugs?
No. Strict mode catches type errors in your own code. Injection comes from untrusted data reaching an interpreter, which the compiler cannot see. Validation and parameterized APIs stop it.
Is unknown safer than any?
Yes, because the compiler forces you to narrow an unknown value before using it. That narrowing step is the right place for a schema check.
Sources
- TypeScript tsconfig reference: strict
- OWASP Node.js Security Cheat Sheet
- OWASP Prototype Pollution Prevention Cheat Sheet
- OWASP Input Validation Cheat Sheet
Last reviewed: 18 October 2026.