“Type checking JavaScript” means two different things. One is a check your code makes while it runs: typeof value === "string". The other is a check a tool makes before the code runs: TypeScript reading your JSDoc comments and reporting that a function is called with the wrong argument. They answer different questions, and most real programs need both.
Two different questions
A runtime check asks “what is this value, right now?” It runs every time the line runs, it sees the actual data, and it costs a little time on each call. It can only tell you about the values that actually arrive.
A static type check asks “can this code ever receive the wrong kind of value?” TypeScript answers it in your editor and in CI, before the code runs, by following every code path. It costs nothing at runtime, but it never sees the real data: a JSON file or an HTTP request can still contain anything.
JavaScript’s own runtime types
JavaScript values carry a type at runtime, and typeof reports it as one of eight strings: "undefined", "boolean", "number", "bigint", "string", "symbol", "function" and "object". A few results surprise people:
| Value | typeof says | What to use instead |
|---|---|---|
null | "object" | value === null |
[1, 2, 3] | "object" | Array.isArray(value) |
NaN | "number" | Number.isNaN(value) |
new Date() | "object" | value instanceof Date |
class User {} | "function" | value instanceof User for its instances |
instanceof checks an object’s prototype chain, so it answers “was this made by that class?” It can give false for an object created in another realm, such as an array from an iframe, which is why Array.isArray exists.
Static annotations
A JSDoc annotation is a comment. Node.js and browsers skip it, so an annotated file runs exactly as it did before. TypeScript reads the comments, combines them with what it can infer, and reports code that cannot be right: a property that does not exist, a string passed where a number is expected, a value that might be null used as if it never is. Nothing is checked while the program runs.
Where they meet: narrowing
TypeScript reads your runtime checks. Inside if (typeof value === "string"), the checker knows value is a string, so the same line that protects the program at runtime also satisfies TypeScript’s checker:
/**
* @param {unknown} value
* @returns {string}
*/
export function describe(value) {
if (typeof value === "string") return `a string of ${value.length} characters`;
if (typeof value === "number") return `the number ${value.toFixed(2)}`;
if (Array.isArray(value)) return `an array of ${value.length} items`;
if (value instanceof Date) return `the date ${value.toISOString()}`;
if (value === null) return "null";
return typeof value;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} The parameter is typed unknown, which means “anything, but prove what it is before you use it”. Each branch narrows it: typeof for primitives, Array.isArray for arrays, instanceof for classes, === null for null.
Data from outside the program
JSON.parse returns any, because TypeScript cannot know what a string will contain. Annotating the result as unknown makes the checker insist on a runtime check before any use:
/** @type {unknown} */
const data = JSON.parse('{"name":"Ada"}');
console.log(data.name);error TS18046: 'data' is of type 'unknown'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A type guard is a function that does that runtime check once and tells TypeScript the result. Its return type, value is User, says “if this returns true, value is a User”:
/**
* @typedef {object} User
* @property {string} name
* @property {number} age
*/
/**
* @param {unknown} value
* @returns {value is User}
*/
export function isUser(value) {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof value.name === "string" &&
"age" in value &&
typeof value.age === "number"
);
}
/** @type {unknown} */
const data = JSON.parse('{"name":"Ada","age":36}');
if (isUser(data)) {
console.log(data.name.toUpperCase(), data.age + 1);
}interface User {
name: string;
age: number;
}
export function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof value.name === "string" &&
"age" in value &&
typeof value.age === "number"
);
}
const data: unknown = JSON.parse('{"name":"Ada","age":36}');
if (isUser(data)) {
console.log(data.name.toUpperCase(), data.age + 1);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} After if (isUser(data)), data is a User inside the branch, and the check runs on the real data every time.
What each one catches
| Runtime checks | Static type checks | |
|---|---|---|
| When | while the code runs | before it runs, in the editor and in CI |
| What it sees | the actual values | every code path, but not the data |
| Cost | a check each time the line runs | nothing at runtime |
| Good at | bad input from users, files, the network and environment variables | typos, wrong arguments, missing null checks, broken refactors |
Using both
Use static types everywhere you control the code, and runtime checks at the edges where data comes in: request bodies, files, environment variables, anything parsed from a string. Write the edge checks as type guards and the two systems reinforce each other: the guard protects the running program, and the type it proves lets TypeScript check everything after it.
The examples on this page were checked with TypeScript 5.9. To set up the static half in your own project, start with the quick start.
Questions
Does a JSDoc type check anything while the program runs?
No. A JSDoc annotation is a comment, so Node.js and browsers skip it. TypeScript reads it before the code runs, in the editor and in CI; at runtime only checks you write, such as typeof and instanceof, run.
Should data from JSON.parse be typed with a cast?
A cast tells the checker to trust you and checks nothing at runtime. Type the result as unknown and narrow it with a type guard, so the shape is checked on the real data before any use.
Sources
- typeof · MDN Web Docs
- instanceof · MDN Web Docs
- Narrowing · TypeScript handbook
- Type Checking JavaScript Files · TypeScript handbook
Last reviewed 20 October 2026.