3 Topics
Chapter•75 Minutes

Runtime Validation, Testing, and Production Tooling

Bridge static types with untrusted data, test TypeScript projects, and keep type safety in CI.

unknown and Runtime Validation

Use unknown when a value's type is not known. Unlike any, unknown prevents unsafe operations until the value is narrowed. Data from JSON.parse, environment variables, local storage, files, and HTTP APIs should be considered untrusted unless a runtime contract is enforced.

A TypeScript interface disappears at runtime, so it cannot validate JSON by itself. Validate with explicit checks or a runtime schema library, then return a typed value only after validation succeeds. Keep validation at system boundaries rather than repeating it throughout trusted internal code.

Narrow unknown data
function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function readName(value: unknown): string | undefined {
  if (!isRecord(value)) return undefined;
  return typeof value.name === "string" ? value.name : undefined;
}
💡 The type predicate narrows the value to a string-keyed object with unknown values. The name property is then checked separately before it is returned as a string.
Key Takeaways
  • Prefer unknown over any at uncertain boundaries.
  • Static types do not validate JSON at runtime.
  • Validate once at boundaries and pass trusted values inward.
Knowledge Check

What is the key safety difference between `unknown` and `any`?

Type Predicates and Assertion Functions

A user-defined type predicate returns a boolean with a type annotation such as value is User. When the function returns true, TypeScript narrows the checked value. Assertion functions use an asserts value is User return annotation and throw or otherwise fail if the condition is not met.

These annotations are promises made by the implementation. If the implementation returns true without checking the required fields, the compiler may trust an incorrect predicate. Keep validators small, test them, and prefer schema libraries for complex nested inputs.

A simple type predicate
type User = { id: string; name: string };

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) return false;
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === "string"
    && typeof candidate.name === "string";
}
💡 The predicate checks the fields that the declared User type requires. A cast is used only to inspect the object after checking it is non-null and object-like; a production validator may need more robust handling for special objects.
Key Takeaways
  • Type predicates communicate narrowing to the compiler.
  • The implementation must genuinely prove the predicate.
  • Complex nested data benefits from dedicated runtime schemas.
Knowledge Check

What does the return annotation `value is User` communicate?

Type Checking, Tests, and Linting

Type checking, runtime tests, and linting catch different classes of defects. Type checking verifies static contracts; unit and integration tests exercise runtime behavior; lint rules enforce conventions and flag suspicious patterns. None replaces the others.

Run all relevant checks in CI. If a test runner or bundler transpiles TypeScript without full type checking, add a separate type-check script. Keep compiler and lint rules aligned with the team's migration stage, and avoid suppressions that accumulate without explanation.

A CI-friendly scripts set
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "lint": "eslint .",
    "check": "npm run typecheck && npm run lint && npm test"
  }
}
💡 The example uses Vitest and ESLint, but equivalent tools are fine. The important idea is that type checking is an explicit step, not assumed to happen because tests run.
Key Takeaways
  • Type checking, tests, and linting have different purposes.
  • Keep a dedicated type-check step in CI.
  • Treat suppression comments as documented exceptions.
Knowledge Check

Which check most directly catches a value assigned to an incompatible TypeScript type?