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.
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;
}name property is then checked separately before it is returned as a string.- Prefer unknown over any at uncertain boundaries.
- Static types do not validate JSON at runtime.
- Validate once at boundaries and pass trusted values inward.
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.
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";
}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.- Type predicates communicate narrowing to the compiler.
- The implementation must genuinely prove the predicate.
- Complex nested data benefits from dedicated runtime schemas.
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.
{
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest run",
"lint": "eslint .",
"check": "npm run typecheck && npm run lint && npm test"
}
}- Type checking, tests, and linting have different purposes.
- Keep a dedicated type-check step in CI.
- Treat suppression comments as documented exceptions.
Which check most directly catches a value assigned to an incompatible TypeScript type?