Home / Blog / TypeScript development /TypeScript Security Best Practices

TypeScript development

October 12, 2026 - by Devico Team

TypeScript Security Best Practices: A Practical Guide for Engineering Teams

A clean tsc --strict build proves that your code is internally consistent. It does not prove that the application can safely handle a malicious request.

TypeScript can catch a number being passed where a string is expected, but it cannot tell whether that string contains a SQL injection payload. The compiler also erases every type before the code runs. By the time attacker-controlled data reaches an API handler, none of the interfaces, annotations, or generics used during development still exist.

The OWASP Top 10:2025 reflects that distinction. Injection ranks fifth among critical web application risks, while Software Supply Chain Failures ranks third. TypeScript does not stop either category on its own.

Secure TypeScript development therefore depends on what happens beyond the type checker: runtime validation, safe output handling, access-control enforcement, dependency management, and CI/CD gates that prevent known weaknesses from reaching production.

Why type safety is not a security boundary

TypeScript checks code against the assumptions encoded in that code. It cannot determine whether those assumptions remain true when data arrives from a browser, webhook, database, message queue, or third-party API.

Consider a parameter declared as email: string. The compiler can verify that the application treats it as a string. It cannot distinguish a valid email address from this value:

'; DROP TABLE users;--

Both satisfy the declared type.

That is the difference between an accidental bug and a security threat. Type checking catches mistakes such as misspelled properties, invalid function arguments, and unhandled null values. An attacker, however, is not making an accidental mistake. They are supplying input that the program accepts but handles unsafely.

This distinction also affects project scope. If an engineering plan treats "the application uses TypeScript" as a completed security task, the controls that reduce actual exposure may never be estimated or assigned. Runtime validation, sanitization, dependency scanning, authorization checks, and security testing still require explicit work.

Where "type-safe" code becomes unsafe

The most misleading TypeScript vulnerabilities often appear in code that looks fully typed. Somewhere near a trust boundary, however, the application has told the compiler to stop checking.

The any escape hatch

The any type disables type checking for the value it touches. Because that value is then passed to other functions and objects, one any at an API boundary can weaken several layers of otherwise strict code.

Explicit any is easy to spot. Type assertions are less obvious:

const response = await fetch(`/api/users/${id}`)
const user = (await response.json()) as User

if (user.role === 'admin') {
  grantAdminAccess()
}

The expression as User does not inspect the response. It instructs the compiler to treat the returned JSON as a User, regardless of its actual contents.

If the endpoint has changed, the response is malformed, or an attacker can manipulate it, the application still trusts fields such as role. The code is statically typed but not runtime-validated.

External data and third-party definitions

The same problem appears when teams describe an API response as { balance: number } and then assume the declaration reflects reality. Network data may arrive incomplete, corrupted, stale, or deliberately altered.

Third-party type definitions are also descriptions rather than runtime guarantees. A declaration from DefinitelyTyped represents the behavior a library is expected to have. If the definitions no longer match the installed package version, the editor may report no errors while production behaves differently.

Every any and as assertion trades certainty for speed. That trade may occasionally be justified, but it should remain visible in code review — especially when the value came from outside the application.

TypeScript can usually catch
TypeScript does not prevent

Misspelled property names

Malicious input

Invalid argument types or order

XSS and injection payloads

null and undefined access with strictNullChecks

Unvalidated API responses

Type mismatches introduced during refactoring

Prototype pollution

Inconsistent internal data models

Compromised dependencies

Some unreachable or contradictory code paths

Broken authorization logic

Validate data when it enters the application

If a value crosses a trust boundary, its TypeScript type should be treated as unknown until runtime validation proves otherwise.

Relevant boundaries include:

  • Request bodies

  • Query and path parameters

  • Webhook payloads

  • Third-party API responses

  • Environment variables

  • Queue messages

  • Cached or persisted JSON

  • Data returned by loosely typed libraries

Schema validation libraries let teams describe the expected structure once, validate real values against it, and derive the static TypeScript type from the same definition. This avoids maintaining separate runtime and compile-time representations that can drift apart.

Zod is a practical default for many backend and full-stack projects. Its schemas can validate a value and provide the corresponding inferred type:

import { z } from 'zod'

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
})

type User = z.infer<typeof UserSchema>

const result = await response.json()
const user = UserSchema.parse(result)

parse() throws if the data does not match the schema. safeParse() returns a typed success or failure result when the application needs to handle validation errors without an exception.

Other libraries suit different constraints:

Library
Best fit
Trade-off

Backend and full-stack applications that want a widely used schema-first approach

Larger browser footprint than more modular alternatives

Valibot

Bundle-sensitive frontends that benefit from a modular, tree-shakeable design

More verbose API and a smaller ecosystem

io-ts

Codebases already using fp-ts and functional programming patterns

Steeper learning curve and more ceremony

Client-side validation remains useful for immediate feedback, but it is not a security control. An attacker can bypass the interface and send a request directly. Any check that protects data, permissions, or business rules must also run on the server.

Preventing XSS and injection in TypeScript applications

The Injection category in the OWASP Top 10:2025 covers 37 weakness types and maps to more than 62,000 CVEs. XSS alone accounts for over 30,000. Every application included in OWASP's tested dataset showed some form of injection weakness.

Typing a value as string has no effect on any of these risks.

Rendering user-controlled HTML

React escapes rendered text by default. That protection ends when code uses dangerouslySetInnerHTML to insert markup directly into the DOM.

A field declared as bio: string may still contain <script>, event handlers, or other executable markup. If the application needs to render user-supplied HTML, sanitize it first:

import DOMPurify from 'dompurify'

const cleanBio = DOMPurify.sanitize(bio)

return <div dangerouslySetInnerHTML={{ __html: cleanBio }} />

DOMPurify removes executable content while retaining markup allowed by its configuration. It should be applied where the HTML is rendered, rather than assuming that a string became safe at some earlier stage.

Raw queries and prototype pollution

ORMs such as Prisma and TypeORM normally parameterize queries. The risk returns when a developer switches to a raw query and interpolates input into the query string.

The rule is the same for SQL and NoSQL operations: pass untrusted values as parameters. Do not concatenate them into executable queries.

Prototype pollution presents a different JavaScript-specific risk. An attacker supplies keys such as __proto__ or constructor, causing an unsafe merge or assignment to modify properties inherited by other objects. Because those keys arrive as runtime data, static typing cannot determine that the operation is dangerous.

No single defense covers every injection path. Framework escaping, runtime validation, contextual output encoding, query parameterization, and a restrictive Content Security Policy address different parts of the problem. The layers matter because one missed validation or rendering mistake should not be enough to compromise the application.

Dependency and supply chain security

Software Supply Chain Failures rose to third place in the OWASP Top 10:2025, replacing the narrower "Vulnerable and Outdated Components" category from the 2021 edition.

This change is particularly relevant to TypeScript projects. A typical npm installation may include hundreds of transitive packages, many of which the team did not select or review directly. Application code can be carefully written and still execute malicious code introduced several levels down the dependency tree.

Sonatype's 2026 State of the Software Supply Chain identified more than 454,000 new malicious open-source packages during 2025. That brought the cumulative number blocked by Sonatype past 1.2 million. More than 99% of the malware detected that year targeted npm.

The same period saw Shai-Hulud, described as the first self-replicating npm worm, compromise hundreds of packages within days. It stole maintainer credentials and used them to publish infected versions of additional packages.

Establish a dependency baseline

npm audit detects installed versions with known vulnerabilities. Dependabot can open update pull requests automatically. Neither tool, however, necessarily identifies a newly compromised package that has not yet been added to a vulnerability database.

Behavior-aware tools such as Socket address part of that gap. They can flag suspicious package behavior — for instance, a new version that suddenly reads environment variables, runs installation scripts, or opens network connections.

A workable baseline also includes the following practices:

  • Verify package names before installation. Typosquatting relies on common misspellings, while slopsquatting uses plausible package names that coding assistants may invent.

  • Keep @types/* packages aligned with the library versions they describe.

  • Commit the lockfile and use npm ci in CI so builds install the versions that were reviewed.

  • Examine lockfile and dependency changes as carefully as application-code changes.

  • Generate a software bill of materials during the build. Enterprise customers increasingly request an SBOM during procurement and security reviews.

  • Define a patching process rather than relying on occasional manual updates.

These habits are also useful when assessing the best TypeScript development companies. A vendor's handling of lockfiles, transitive dependencies, suspicious package behavior, and security updates usually reveals more about its day-to-day security discipline than a polished code sample.

Authentication, authorization, and secrets

Broken Access Control holds the number-one position in the OWASP Top 10:2025. It is usually a logic failure, not a type failure.

A handler can return a correctly typed Invoice object and still expose another customer's invoice because it never checked ownership. A union type can describe the permitted roles, but the server must enforce those permissions on every protected operation.

JWT mistakes TypeScript cannot catch

Common JSON Web Token failures include:

  • Storing tokens in localStorage, where an XSS payload can read them

  • Issuing long-lived access tokens that increase the impact of a leak

  • Failing to rotate refresh tokens

  • Trusting a role or permission supplied by the client

  • Accepting the algorithm specified in the token header instead of pinning the expected algorithm on the server

An httpOnly cookie is generally safer than browser-accessible storage for sensitive tokens. Access tokens should have short lifetimes, while refresh-token rotation limits the usefulness of stolen credentials.

Algorithm confusion deserves particular attention. If the server accepts an algorithm chosen by the token itself, an attacker may attempt to downgrade verification or use none. The expected algorithm should be fixed in server configuration and enforced according to the OWASP JWT guidance.

Validate configuration before the application starts

Environment variables are another runtime boundary. TypeScript correctly treats process.env.API_KEY as string | undefined, but developers often assert it to string and continue.

That can turn a missing secret into a silent production error or trigger an insecure fallback. Instead, validate configuration once during startup:

import { z } from 'zod'

const EnvironmentSchema = z.object({
  API_KEY: z.string().min(1),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
})

const env = EnvironmentSchema.parse(process.env)

The process now fails immediately when required configuration is missing or malformed. Secrets should remain outside the repository and client bundle, with storage and rotation based on the OWASP secrets-management practices.

Put security checks into CI/CD

Security practices that depend on someone remembering them will be applied unevenly. The pipeline should reject known unsafe patterns before they are merged.

Begin with strict mode, then go further

tsc --strict enables strictNullChecks, noImplicitAny, and the rest of TypeScript's strict family. It removes several common sources of defects, but explicit any, unchecked assertions, missing runtime validation, and insecure data handling remain possible.

Strict mode is therefore the starting point rather than the security policy.

Add linting and static analysis

typescript-eslint is the standard linting toolchain for modern TypeScript projects. Security-focused plugins can extend it:

A practical pipeline might enforce these rules:

  • tsc --strict must pass.

  • New explicit any values and unsafe assertions require review.

  • Security lint violations fail the build.

  • SAST runs on each pull request.

  • Dependency and behavior scans run during installation.

  • New eslint-disable, @ts-ignore, and as any additions are reviewed rather than accepted as routine build fixes.

Pipeline enforcement becomes especially important when several teams or vendors contribute to the same codebase. If you hire TypeScript developers to extend an existing team, assess whether they understand and respect these controls — not simply whether they know the language syntax.

TypeScript security maturity checklist

Score one point for every statement your team can answer "yes" to today. Planned work does not count.

Category
Criterion

Type discipline

☐ No new explicit any is merged without review sign-off

Type discipline

☐ Type assertions are reviewed at trust boundaries

Type discipline

☐ tsc --strict passes without suppressions

Input validation

☐ Every inbound boundary is runtime-validated with Zod, Valibot, io-ts, or an equivalent tool

Input validation

☐ Security-relevant validation runs on the server rather than only in the client

Input validation

☐ External API responses are parsed instead of asserted

Input validation

☐ Environment configuration is schema-validated at startup

Dependency hygiene

☐ Dependency scanning runs during every installation and includes behavior-aware analysis

Dependency hygiene

☐ Automated dependency update pull requests are enabled

Dependency hygiene

☐ The lockfile is committed and CI uses npm ci

Dependency hygiene

☐ An SBOM is generated during the build

Authentication and secrets

☐ Access control is enforced server-side on every protected route

Authentication and secrets

☐ JWT algorithms are pinned and access-token lifetimes are short

Authentication and secrets

☐ Secrets never enter the repository or client bundle

CI/CD enforcement

☐ Security linting gates merges

CI/CD enforcement

☐ SAST runs on every pull request

CI/CD enforcement

☐ New eslint-disable and as any additions receive substantive review

Scoring

  • 0–7 — Foundational: The application still relies heavily on TypeScript itself as a security control. Start with server-side validation and dependency scanning.

  • 8–13 — Developing: Relevant practices exist, but their application is uneven. Prioritize enforcement so security does not depend on individual diligence.

  • 14–17 — Mature: Security is embedded in the delivery pipeline. The remaining work is to keep schemas, rules, scanners, and dependency policies current as the application changes.

Build the capability internally or add external expertise?

The checklist identifies weak controls, but it does not decide who should fix them. That depends on whether the gap is temporary, recurring, or large enough to justify a dedicated function.

Training the existing development team makes sense when secure TypeScript development will remain an ongoing requirement and senior engineers can lead the change. The trade-off is time. New review habits and pipeline standards may take months to become consistent, and the work competes with product delivery.

Hiring application-security specialists is appropriate once the organization has enough ongoing security work to support a permanent function. Recruitment can be slow, however, and a standing AppSec team may be more than a single modernization project or fixed compliance deadline requires.

Adding vetted senior engineers can suit a defined project: hardening an API, preparing for an audit, modernizing a vulnerable codebase, or covering a specific capability gap. It provides immediate capacity without creating a permanent role, but the engineers still need to work within enforceable standards.

Whichever model you choose, test for runtime security judgment rather than TypeScript fluency alone. Ask the candidate or vendor to harden a concrete endpoint. A strong answer should move beyond interfaces and union types to input schemas, access-control checks, output handling, dependency risk, testing, and CI enforcement.

TypeScript improves security indirectly by removing certain defects before they reach production. It does not create a runtime security boundary. That boundary must be built deliberately — with validation at every entry point, layered protection against injection, disciplined dependency management, server-side authorization, and pipeline checks that fail when those standards regress.

Frequently asked questions

Is TypeScript more secure than JavaScript?

It prevents some type-related mistakes that could otherwise contribute to security problems. It does not add runtime protection against malicious input, XSS, injection, broken access control, or compromised dependencies.

A TypeScript application without runtime validation remains as exposed to hostile input as an equivalent JavaScript application.

Does TypeScript prevent XSS attacks?

No. A value typed as string can still contain executable markup.

React escapes normal text output by default, but applications must sanitize HTML inserted directly into the DOM. A restrictive Content Security Policy provides another layer of protection if unsafe content reaches the page.

What is the biggest TypeScript security mistake?

The most consequential recurring mistake is treating a compile-time declaration as proof of runtime data.

This often appears as an assertion such as response as User. The assertion does not validate the response; it moves untrusted data into a trusted code path without checking it.

Do I need runtime validation if I already use TypeScript?

Yes. TypeScript types are removed during compilation and cannot inspect data received while the application is running.

Libraries such as Zod, Valibot, and io-ts validate actual values. They can also derive static types from the same schemas, keeping runtime validation and TypeScript definitions aligned.

Stay in touch

Leave your email and we will inform you about all our news and updates

 

Up next