completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Types for JavaScript: JSDoc & TypeScript

Learn how JSDoc, checkJs, TypeScript, and declaration files catch JavaScript type mistakes before runtime while keeping validation honest.

By the end, you can
  • 01
    Add types to JavaScriptWrite types in JSDoc comments with tags such as @param, @returns, @typedef, @property, @template, and @import.
  • 02
    Check code before it runsTurn on // @ts-check, checkJs, and strict TypeScript, so mistakes show up in your editor before users see a crash.
  • 03
    Pick the right toolKnow when TypeScript, .d.ts declaration files, and runtime checks each protect your code.

Find type mistakes before your users do

A type is the kind of a value: a number, a string, an object, a function, and so on. JavaScript checks types only while the code runs. That time is called runtime. A variable can hold a number now and a string later. This is flexible, but some mistakes stay hidden until runtime:

  • a property name with a typo;
  • a missing callback, which is a function you pass in so it can be called later;
  • calling something that is not a function, which throws a TypeError.

If the mistake sits on a path that rarely runs, a user may be the first person to find it.

Definition

Static type checking means a tool reads your code before it runs. "Static" means "without running". The tool is called a type checker. It works out the type of each value, parameter, property, and return value. When something doesn't fit, it shows a warning: a wavy line in your editor, or an error message in the terminal. It never runs your program.

This lesson builds on data types, checking types, and type coercion. At runtime, JavaScript still uses the tools you know: typeof, Array.isArray, truthy checks, and if checks. The new tool in this lesson is a checker that can warn you before the code runs.

Real-life analogyA proofreader before printing

A proofreader reads an article before it is printed. They mark a missing word, a misspelled name, or a sentence that makes no sense. They can't tell you whether the facts are true. A type checker does the same job for your code. It marks mistakes before anyone runs the code.

In real life: The proofreader marks a missing word before printing
In JavaScript: The checker marks a missing property before running
In real life: The writer still checks that the facts are true
In JavaScript: Your code still checks data that comes from outside
In real life: A style guide gives everyone the same rules
In JavaScript: JSDoc, TypeScript, and .d.ts files give the checker its rules
A missing callback crashes at runtimePop out in the code editor (opens in a new tab)JavaScript
const lesson = {  title: "Types",  onComplete: undefined,}; lesson.onComplete();

Line 3 sets onComplete to undefined. Line 6 calls it anyway. JavaScript finds out only when it reaches line 6, so it throws a TypeError. A type checker could warn you about line 6 while you type.

Left: a type checker reads lesson.onComplete() in the editor and warns before the code runs. Right: without a checker, the browser runs the line and the console shows TypeError: lesson.onComplete is not a function.the checker reads the codeeditorlesson.onComplete();warned before it runsthe browser runs the codeconsoleTypeErrorlesson.onComplete is not a functioncrashes when it runs
The same line, found at two different times. A checker warns while you type. Without one, the mistake waits until the browser runs the line.

A callback is optional when it may be missing. Your code must treat it that way: check that it is a function before you call it.

Check an optional callback before calling itPop out in the code editor (opens in a new tab)JavaScript
const lesson = { onComplete: undefined };
const finish = lesson.onComplete;
if (typeof finish === "function") finish();
console.log("Lesson saved");

Line 2 reads the callback, which may be missing. Line 3 calls it only if it is a function. Here it is undefined, so the call is skipped. Line 4 prints Lesson saved and nothing throws. This check also works for data that comes from outside your code.

A checker finds some mistakes early. It can't prove that your code does the right thing, for example that the lesson was really saved. Tests and runtime checks answer those questions.

The table compares three tools. Each row gives the tool, what it is good at, and what it can't do.

Three tools that protect your code
ToolWhat does it catch?What can't it do?
JSDoc + checkJsYou keep .js files and add comments. Good for adding types bit by bit, and for libraries that don't want a build step.The comments can get long. Complex types are harder to read than in TypeScript.
TypeScript filesYou write .ts or .tsx files with type syntax. You get inference, strict mode, unions, generics, and strong editor help.The types are removed before the code runs. You still need a tool that compiles or strips them.
Runtime checksReal JavaScript checks on data from APIs, forms, storage, and URLs.They protect real data, but they don't warn you about your own calls while you type.

JSDoc: types written in comments

COMMENTS AS TYPES

JSDoc is a way to write types inside comments. You keep your normal .js files. You add special comments that start with /**. Editors that use TypeScript can read them.

JavaScript skips comments when it runs, so these comments change nothing at runtime. To make the checker read them, put // @ts-check at the top of a file, or turn on checkJs for the whole project. The next section shows how.

A function with a JSDoc typePop out in the code editor (opens in a new tab)JavaScript
// @ts-check
/** @param {number} price @returns {number} */
function withTax(price) {
  return price + 2;
}
console.log(withTax(3));

Line 2 tells the checker two things: price is a number, and the function returns a number. Line 4 adds a fee of 2, so the call on line 6 prints 5. The comment doesn't change how the code runs. If you call withTax("3"), JavaScript still joins the text and prints 32, unless your code checks the input.

Each row of the next table is one JSDoc tag: what you write, what it does, and an example.

JSDoc tags that TypeScript understands
What you writeWhat it doesExample
@param {Type} nameGives a function parameter a type.@param {number} price
@returns {Type}Gives the value a function returns a type.@returns {string}
@type {Type}Gives a variable, value, or property a type./** @type {CourseCard} */
@typedef + @propertyNames an object shape, so you can use it again.title, lessons, and an optional status
[name] or [name=default]Marks a parameter or property as optional, with a default value.@param {number} [limit=10]
A | BA union: the value can be type A or type B.string | number
@template TMakes a generic: T stands for whatever type the caller passes.first<T>(items): T | undefined
@import {Type} from "./file"Brings in a type name for comments only. Needs TypeScript 5.5 or newer.No code is loaded at runtime.

Name an object shape, with optional and either-or types

The next example uses a few new tags and words:

  • @typedef gives a name to an object shape. A shape is the list of properties an object has, with their types. Here the name is CourseCard.
  • @property adds one property to that shape.
  • Square brackets, like [status="draft"], mark a property as optional. The part after = is its default value.
  • string | number is a union: the value can be a string or a number.
A JSDoc object shape and an optional parameterPop out in the code editor (opens in a new tab)JavaScript
// @ts-check/** * @typedef {object} CourseCard * @property {string} title * @property {number} lessons * @property {"draft" | "published"} [status="draft"] */ /** * @param {CourseCard} course * @param {string | number} [prefix="Course"] * @returns {string} */function summarize(course, prefix = "Course") {  const label = typeof prefix === "number" ? "#" + prefix : prefix;  return label + ": " + course.title + " (" + course.lessons + ")";} console.log(summarize({ title: "Types", lessons: 4, status: "published" }, 7));

Read it line by line:

  • Lines 3 to 5 name the shape CourseCard: a string title and a number lessons.
  • Line 6 adds an optional status. It can only be "draft" or "published".
  • Line 10 says course must have the CourseCard shape.
  • Line 11 says prefix is optional, and it can be a string or a number.
  • Line 15 uses typeof to find out which one it got. A number such as 7 becomes "#7".
  • Line 19 passes 7, so it prints #7: Types (4).

A union only lists what a value might be. Your code still has to check which one it is, as line 15 does.

Keep the item type with @template

A generic function works with many types and remembers which one it got. @template T makes a stand-in name, T, that means "whatever type the caller passes". Here, T is the type of the array's items. The function returns an item of that same type.

A generic first function in JSDocPop out in the code editor (opens in a new tab)JavaScript
// @ts-check/** * @template T * @param {T[]} items * @returns {T | undefined} */function first(items) {  return items[0];} const firstName = first(["Ada", "Lin"]);console.log(firstName);

Line 4 says items is an array of T. Line 11 passes an array of strings, so here T is string. The first item is Ada, so line 12 prints Ada.

Line 5 says the result is T | undefined. So the checker treats firstName as string | undefined. Why undefined? Because an array can be empty:

An empty array has no first itemPop out in the code editor (opens in a new tab)JavaScript
// @ts-check
/** @template T @param {T[]} items @returns {T | undefined} */
function first(items) {
  return items[0];
}
console.log(first([]) ?? "No items");

Line 6 calls first([]). Line 4 returns items[0], which is undefined for an empty array. The ?? operator gives a fallback value when the left side is undefined or null, so line 6 prints No items. The generic keeps the item type. It doesn't promise that an item exists.

Import a type without importing code

Sometimes a type is written in another file. TypeScript 5.5 added the JSDoc @import tag for this. It brings in a type name for your comments only. It loads no code at runtime.

An older way also works: write {import("./types").Course} inside the comment. @import is easier to read when several comments use the same type.

This example needs a second file, types.d.ts, where the type is written. The editor on this page can't check that file, so this block is for reading only.

Import a type in a JSDoc commentJavaScript
// @ts-check/** @import {CourseFromTypes} from "./types" */ /** @type {CourseFromTypes} */const importedCard = { title: "JSDoc", lessons: 2 }; console.log(importedCard.title);

Line 2 imports the type name CourseFromTypes. Line 4 uses it as the type of importedCard on line 5. Line 7 prints JSDoc.

You don't have to write every type. The checker can often work out a type by itself from the values. This is called inference. The panel below shows the types the real checker worked out for some of the examples in this lesson.

Types the checker worked out by itself
firstName on line 11string | undefined
importedCard on line 5CourseFromTypes
Try it yourself

Each type here comes from the real TypeScript checker, which this lesson's test runs. None of them is a guess.

Each row names a variable, its line in the example, and the type the checker gave it. The page shows saved results, so your browser doesn't need to load TypeScript.

Turn on checkJs

REAL DIAGNOSTICS

JSDoc comments help only when the checker is running. There are two ways to turn it on:

  • For one file: put // @ts-check on the first line.
  • For the whole project: set allowJs and checkJs to true in a jsconfig.json or tsconfig.json file.

Also set strict to true. It turns on extra checks for values that may be missing, such as undefined and null. It also makes the unknown type, which you'll meet later, much more useful.

Now the checker can catch withTax("3") before it prints 32. The JSDoc says price is a number, but that call gives it a string.

A warning in your editor is easy to miss. So also run tsc --noEmit in continuous integration (CI): the automatic checks that run on every change before it ships. tsc is the TypeScript checker, and --noEmit means "check only, don't write any files".

Avoid // @ts-nocheck. It turns off checking for the whole file. Use it only for a short time, while you fix the file.

Turn on checking for every JavaScript fileJSON
{  "compilerOptions": {    "allowJs": true,    "checkJs": true,    "strict": true,    "noEmit": true  },  "include": ["src/**/*.js"]}

Line 3 includes JavaScript files. Line 4 reports errors in them. Line 5 turns on the stricter checks.

An error message from the checker is called a diagnostic. Each one has a code such as TS2722, a line number, and a message. Pick an example below to see its real diagnostic.

What the type checker reports
Checked code: Optional callbackJavaScript
// @ts-check/** * @typedef {{ title: string, onComplete?: () => void }} Lesson */ /** @param {Lesson} lesson */function finishLesson(lesson) {  lesson.onComplete();} 
What the checker saysTypeScript 5.9
TS2722line 8

Cannot invoke an object which is possibly 'undefined'.

Try it yourself

Pick an example to see what the checker reports for it. The page shows saved results. This lesson's test runs the real TypeScript 5.9 checker and makes sure each code, line, and message matches.

This panel shows saved checker results; it doesn't run TypeScript in your browser. The other JavaScript examples in this lesson still run with Edit & run.

The big change is when you find the mistake. Without a checker, the missing callback throws only at runtime. With a checker, TS2722 shows up in your editor before you open the browser. With a named object shape, the checker also catches a property name that looks right but doesn't exist, such as lessonCount.

TypeScript at a glance

TYPES ARE REMOVED

TypeScript is JavaScript plus a way to write types in the code itself, plus a checker. Its files end in .ts. You write a type after a colon. This is called an annotation:

  • const price: number = 3 gives a variable a type.
  • function greet(user: User): string gives a parameter a type, and says the function returns a string.

You don't need to annotate everything. TypeScript works out many types from the values, as the checker did for JSDoc. There are two ways to give a type a name:

  • A type alias can name any type: an object shape, or a union such as string | number.
  • An interface names only an object shape. You can extend it later.

Many teams use both, and pick whichever reads more clearly.

Name an object shape in TypeScriptTypeScript
type User = { name: string };
function greet(user: User): string {
  return "Hello, " + user.name;
}
console.log(greet({ name: "Asha" }));

Run this in a TypeScript project, not in this page's editor. Line 1 names the shape User: an object with a string name. Line 2 says greet takes a User and returns a string. Line 3 reads the name. Line 5 prints Hello, Asha.

Writing interface User { name: string } would name the same shape. But a union such as string | number needs a type alias.

The next example uses a union of three object shapes. Each shape has a status property with a different fixed value, so status tells the shapes apart. Narrowing means the checker learns more about a value from an if check. Inside the if, it knows which shape you have.

Narrow a union by checking a propertyPop out in the code editor (opens in a new tab)TypeScript
type ApiState =  | { status: "loading" }  | { status: "ready"; user: { name: string } }  | { status: "error"; message: string }; function label(state: ApiState) {  if (state.status === "ready") {    const readyState = state;    return readyState.user.name.toUpperCase();  }  if ("message" in state) {    return state.message;  }  return "Loading";} const result = label({ status: "ready", user: { name: "Ada" } });console.log(result);
  • Lines 1 to 4 say an ApiState is loading, ready, or error.
  • Line 7 checks state.status === "ready". Inside this if, the checker knows it has the ready shape, so user exists.
  • Line 11 checks "message" in state. The in operator asks: "does this object have a property with this name?" Only the error shape has message, so inside this if the checker knows it has the error shape.
  • Line 17 passes a ready object, so line 18 prints ADA.

In the panel in the JSDoc section, press Add the TypeScript example. It shows the types the checker found for readyState and result. This lesson's test gets them from the real TypeScript checker, so they are not guesses.

Generics

A generic function remembers a type. For example, the type of the items that go in is the type of the item that comes out.

unknown vs any

unknown makes you check a value before you use it. any turns off checking for that value.

Strict mode

Turns on extra checks for missing values, such as undefined and null, and for values with no type at all.

Here is unknown in action. Line 2 reads payload.name without checking what payload is first, so the checker reports an error.

unknown must be checked before useTypeScript
function readName(payload: unknown) {  return payload.name;} 

Here is a very common TypeScript error. Line 1 says lessons must be a number. Line 2 gives it the string "four", so the checker reports TS2322.

A string where a number should beTypeScript
type Course = { title: string; lessons: number };const course: Course = { title: "Types", lessons: "four" }; 

A type assertion, written with as, tells the checker "trust me, this is a number". It doesn't change the value. In the next example the value stays a string at runtime.

as doesn't turn a string into a numberTypeScript
const price = "3" as unknown as number;
console.log(price + 1);

In a TypeScript project, line 1 claims "3" is a number, but it stays the string "3". Line 2 joins text, so it prints 31, not 4. To really turn text into a number, use Number(price) and check that the result is a real number.

Types are removed before the code runs

TypeScript adds no checks at runtime. Browsers run only JavaScript. The editor on this site can run TypeScript examples because it removes the types first. Removing the types is called type stripping.

Node 22.18+ and Node 23.6+ also strip types by default. But:

  • Node doesn't check the types.
  • Node doesn't read the paths setting in tsconfig.
  • Some TypeScript features turn into real JavaScript code, so they can't just be removed. Examples are enums, parameter properties, import aliases, and namespaces that hold code. These need the --experimental-transform-types flag or a full TypeScript tool.
  • Node doesn't run .tsx files, and it doesn't strip types in packages inside node_modules.

So stripping is not a type check. Still run tsc --noEmit on its own.

For example, Node 22.18+ can run the next .ts file directly. It removes : number from line 1. Then line 2 prints 4. Run it in Node, not in this page's editor.

Node removes a type before runningTypeScript
const price: number = 3;
console.log(price + 1);
Left: a price.ts window holds const price: number = 3; and console.log(price + 1);. An arrow labelled remove types points right. Right: a Node runs window holds const price = 3; and console.log(price + 1);. It prints 4.price.tsconst price: number = 3;console.log(price + 1);removetypesNode runsconst price = 3;console.log(price + 1);the type is for the checkerprints 4
Node removes : number before it runs the file. The type is gone, so it can't check anything at runtime.

There is also a plan to add type syntax to JavaScript itself. TC39 is the group that designs JavaScript. Its Type Annotations proposal is at Stage 1, the earliest stage. The idea is like type stripping: JavaScript engines would skip type annotations as if they were comments, and separate tools would check them.

It would not support every TypeScript feature; enums, for example, are left out. It is only a proposal. You can't use it in browsers today.

Narrowing and real data at runtime

STEP THROUGH

The checker narrows types by reading your code. A runtime check, also called a validator, looks at the real data while the code runs. You need both where data comes into your code from outside:

  • responses from fetch;
  • text you read with JSON.parse;
  • values saved in localStorage;
  • URLs and form fields;
  • messages from another window.

An API's documentation may promise a field, and the real data may still be missing it. So check the real value before you show a price:

Turn away an order with no pricePop out in the code editor (opens in a new tab)JavaScript
const data = JSON.parse('{"title":"Tea"}');
const isOrder = typeof data === "object" && data !== null &&
  typeof data.title === "string" && typeof data.price === "number";
console.log(isOrder ? data.price : "Invalid order");

Line 1 reads data that has no price. Lines 2 and 3 check three things: data is an object, title is a string, and price is a number. The price is missing, so isOrder is false. Line 4 prints Invalid order.

Checking only that data is an object would not be enough. The price would still be missing when you show it.

The parsed data is a cart holding only title: "Tea", with no price. An arrow leads to the check, isOrder: false. Another arrow leads to the printed text, Invalid order.title: "Tea"datano price insideisOrder: falsethe checkInvalid orderprinted
The data has no price, so the check gives false. The code prints Invalid order instead of showing a missing price.

Now step through a bigger validator. Pick good or broken data, and watch which branch runs.

A runtime check for data from an API
Step 0 of 6Ready
Your turn: follow the blue line

Step through a real runtime check. Types help while you write code; this check protects the place where outside data comes in.

Running in
  1. script
Next: line 1
  return typeof value === "object" &&    value !== null &&    typeof value.title === "string" &&    Array.isArray(value.lessons) &&    value.lessons.every((lesson) => typeof lesson === "string");} const payload = JSON.parse('{"title":"Types for JS","lessons":["JSDoc","TypeScript"]}');if (isCourse(payload)) {  console.log(payload.title.toUpperCase());} else {  console.log("Invalid course data");}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the JSON data

Picking other data starts the replay again. The check is real JavaScript, so it protects data while the code runs.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The guard is plain JavaScript. In TypeScript you can give it a special return type, value is CoursePayload. This is called a type predicate. It tells the checker: "if this function returns true, the value has the CoursePayload shape". So the same if protects users at runtime and teaches the checker.

How can a checker find a mistake without running the code? The toy checker below shows the idea. It reads the parameters a function expects and the arguments a call passes, then compares them.

A toy checker reads a call without running it
Step 0 of 5Ready
Your turn: follow the blue line

A tiny checker shows the idea: it reads facts from the code and reports problems without running the function it checks.

Running in
  1. script
Next: line 1
  name: "enroll",  params: [    { name: "student", type: "string" },    { name: "lessons", type: "number" },  ],}; function checkCall(signature, args) {  const errors = [];  if (args.length !== signature.params.length) {    errors.push(`Expected ${signature.params.length} arguments, got ${args.length}.`);  }  signature.params.forEach((param, index) => {    if (index < args.length && typeof args[index] !== param.type) {      errors.push(`${param.name} should be ${param.type}, got ${typeof args[index]}.`);    }  });  return errors;} const report = checkCall(signature, ["Ada", "4"]);console.log(report.join("\\n") || "Call matches the signature.");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the call

This checker is tiny on purpose. It only counts the arguments and compares their typeof results.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The toy checker is tiny on purpose. It only counts arguments and compares typeof results. Real TypeScript does much more:

  • it reads every file in the project;
  • it reads .d.ts files;
  • it follows imports;
  • it follows if checks and early returns;
  • it compares complex types.

The main idea is the same: it reports a bad call without running the function.

Declaration files: types for JavaScript that already exists

.D.TS

A declaration file ends in .d.ts. It describes types, but it holds no working code. TypeScript uses declaration files to understand:

  • JavaScript packages;
  • browser APIs, such as document;
  • Node APIs;
  • global variables;
  • older scripts.

A declaration file only describes. If it is out of date, it can say something exists when it doesn't:

A description can't add a missing methodPop out in the code editor (opens in a new tab)JavaScript
const widget = {};
console.log(typeof widget.mount);

Line 1 makes an empty widget. Line 2 prints undefined, even if a .d.ts file says mount exists. Keep declaration files in step with the package you installed. Test the real code too; don't trust a description that may be old.

A tag that says mount(), from the .d.ts file, points at the real widget. The widget cart is empty. typeof widget.mount gives undefined.the .d.ts saysmount()the real widgetwidgettypeof widget.mountundefined
The .d.ts file only describes mount(). The real widget is empty, so typeof widget.mount prints undefined.

Here is a small declaration file for an older widget module. The word declare means "this exists at runtime; I'm only describing it".

A .d.ts file for an older widget moduleTypeScript
declare module "legacy-widget" {  export type WidgetOptions = {    theme?: "light" | "dark";    onClose?: () => void;  };   export function mount(target: Element, options?: WidgetOptions): void;}

Line 1 describes a module named legacy-widget. Line 2 names the shape of its options. Line 7 describes the mount function. None of these lines make anything at runtime.

Each row of the next table is one word you'll meet around declaration files: the word, what it means, and where you see it.

Words used with declaration files
WordWhat it meansWhere you see it
.d.tsA file that describes types for JavaScript that lives somewhere else.Next to a package's code, or in a separate @types/name package.
declareSays a value, module, or global exists at runtime. This file only describes it.declare module "legacy-widget" { ... }
types fieldTells TypeScript where a package's main declaration file is."types": "./dist/index.d.ts" in package.json
exports fieldCan give a declaration file for each path a package exports."types": "./dist/index.d.ts" inside one export entry
DefinitelyTypedA community project that publishes declaration files as @types/* packages.npm i -D @types/node for Node's globals.
Declaration files from JSDocTypeScript can read checked .js files and write .d.ts files.Turn on allowJs, checkJs, declaration, and emitDeclarationOnly.

A package can tell TypeScript where its declaration files are in two ways:

  • a top-level types field in its package.json;
  • a types path inside each entry of its exports field.

Some packages include no declaration files. Then a community project called DefinitelyTyped may publish them as a separate package whose name starts with @types/.

Make declaration files from JSDoc

TypeScript can also read JSDoc-typed JavaScript and write .d.ts files for you. Then people who use your JavaScript library get types too. This lesson's test does exactly that with the lesson's JSDoc code. It checks that the result matches the file shown below.

Write only .d.ts files from checked JavaScriptJSON
{  "compilerOptions": {    "allowJs": true,    "checkJs": true,    "declaration": true,    "emitDeclarationOnly": true,    "outDir": "types"  },  "include": ["src/**/*.js"]}

Line 4 checks the JavaScript files. Line 5 turns on declaration files. Line 6 writes only declaration files, with no JavaScript. Line 7 puts them in a folder named types.

The .d.ts file TypeScript wrote from the JSDocTypeScript
/** * @typedef {object} CourseSummary * @property {string} title * @property {number} lessons *//** * @param {CourseSummary} course * @returns {string} */export function formatCourse(course: CourseSummary): string;export type CourseSummary = {    title: string;    lessons: number;}; 

Line 10 is the function's type: it takes a CourseSummary and returns a string. Lines 11 to 14 turn the JSDoc @typedef into a TypeScript type. The JSDoc comments are kept above them.

In real projects: pick the right tool

SORT IT

Real teams rarely use types alone. Each tool has its own job:

  • Types check calls and object shapes.
  • Runtime checks protect data you don't control.
  • A linter, a tool that checks code style and common slips, keeps the code tidy.
  • Tests check that the program does the right thing.

Types are great at finding operations that can never work. Tests are great at showing that important features still work.

API data may not match its typePop out in the code editor (opens in a new tab)JavaScript
const data = JSON.parse('{"price":"3"}');
console.log(typeof data.price);

Line 1 reads data where the price is in quotes, so it is a string. Line 2 prints string, not number. Writing a type such as { price: number } for this data, or using as, can't change the data.

So check or convert the data where it enters your code. After that check, normal TypeScript types keep the rest of the app consistent.

A gentle way to add types to a JavaScript project:

  • Start with // @ts-check in one risky JavaScript file.
  • Add JSDoc where the checker can't work out a type, especially for function parameters.
  • Move to .ts files when type syntax is easier to read than comments.
  • Keep runtime checks for API data and user input, even after you move to TypeScript.
Which tool catches it first?
  • `course.lenght` when `course` has the type `{ length: number }`
  • `JSON.parse(response).lessons` from an API you don't control
  • `button.addEventListener("click", 42)`
  • A user submits an empty password
  • A local variable is created but never used
  • A list is sorted smallest first, but the product needs largest first
Try it yourself
0 of 6 correct

Sort each situation by the tool that catches it first and reliably. Some bugs need tests or a linter, not types.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “JSDoc always checks my JavaScript.” JSDoc is only comments until // @ts-check or checkJs tells TypeScript to read it.
  • “TypeScript checks API data for me.” Types are removed before the code runs. Data from outside still needs a runtime check.
  • “any is a quick fix.” Sometimes you need it where outside data comes in, but it also hides mistakes. Use unknown until you have checked the value.
  • “A .d.ts file holds the module's code.” It only describes a module. The real code must exist at runtime.
  • “If Node can run my TypeScript, my types are checked.” Node only removes the types. It doesn't check them.

The next table compares three kinds of tools. Each row asks the same question about all three.

Three safety tools with different jobs
QuestionStatic typesRuntime checksLinting and tests
When does it run?Before the code runs: in the editor, in tsc, or in CI.While the program runs.Linting runs before the code runs. Tests run chosen parts of the program.
What is it best at?Calls, property names, missing values, and operations that can never work.Data you don't control: from APIs, users, storage, and URLs.Code style, accessibility rules, behavior, and features that broke again.
What is the warning sign?Hiding errors with any, or JSDoc that is out of date.Expecting typed code to check unknown data.Testing only the normal path, or following lint rules without understanding them.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the output of JSDoc-typed code

Read the code. Type exactly what the last line prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
// @ts-check
/**
 * @typedef {object} CourseCard
 * @property {string} title
 * @property {number} lessons
 * @property {"draft" | "published"} [status="draft"]
 */

/**
 * @param {CourseCard} course
 * @param {string | number} [prefix="Course"]
 * @returns {string}
 */
function summarize(course, prefix = "Course") {
  const label = typeof prefix === "number" ? "#" + prefix : prefix;
  return label + ": " + course.title + " (" + course.lessons + ")";
}

console.log(summarize({ title: "Types", lessons: 4, status: "published" }, 7));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeName the error code for an optional callback

    Type the diagnostic code that the checker reports for the unsafe call.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // @ts-check
    /**
     * @typedef {{ title: string, onComplete?: () => void }} Lesson
     */
    
    /** @param {Lesson} lesson */
    function finishLesson(lesson) {
      lesson.onComplete();
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWhat does the generic first print?

      Type the name that this JSDoc generic example prints.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      // @ts-check
      /**
       * @template T
       * @param {T[]} items
       * @returns {T | undefined}
       */
      function first(items) {
        return items[0];
      }
      
      const firstName = first(["Ada", "Lin"]);
      console.log(firstName);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWhat does the ready branch print?

        Predict what this JavaScript version of the ready branch prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const state = { status: "ready", user: { name: "Ada" } };
        function label(state) {
          if (state.status === "ready") {
            return state.user.name.toUpperCase();
          }
          if ("message" in state) {
            return state.message;
          }
          return "Loading";
        }
        console.log(label(state));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFind the name prefix for community type packages

          A JavaScript package doesn't include its own types. Which package name prefix usually gives you declaration files written by the community?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does a static type checker do for JavaScript?

              Choose an answer to see the explanation.

            2. Question 2 of 8Which real diagnostic does the checker report for this JavaScript?

              Read the code, then predictJavaScript
              // @ts-check
              /**
               * @typedef {{ title: string, onComplete?: () => void }} Lesson
               */
              
              /** @param {Lesson} lesson */
              function finishLesson(lesson) {
                lesson.onComplete();
              }
              

              Choose an answer to see the explanation.

            3. Question 3 of 8What does the JSDoc object-shape example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              // @ts-check
              /**
               * @typedef {object} CourseCard
               * @property {string} title
               * @property {number} lessons
               * @property {"draft" | "published"} [status="draft"]
               */
              
              /**
               * @param {CourseCard} course
               * @param {string | number} [prefix="Course"]
               * @returns {string}
               */
              function summarize(course, prefix = "Course") {
                const label = typeof prefix === "number" ? "#" + prefix : prefix;
                return label + ": " + course.title + " (" + course.lessons + ")";
              }
              
              console.log(summarize({ title: "Types", lessons: 4, status: "published" }, 7));

              Choose an answer to see the explanation.

            4. Question 4 of 8Which settings turn on checking for every JavaScript file in a project?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does this JavaScript version of the narrowing example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const state = { status: "ready", user: { name: "Ada" } };
              function label(state) {
                if (state.status === "ready") {
                  return state.user.name.toUpperCase();
                }
                if ("message" in state) {
                  return state.message;
                }
                return "Loading";
              }
              console.log(label(state));

              Choose an answer to see the explanation.

            6. Question 6 of 8Why use unknown instead of any for data from outside your code?

              Choose an answer to see the explanation.

            7. Question 7 of 8What is true about Node's built-in TypeScript type stripping?

              Choose an answer to see the explanation.

            8. Question 8 of 8What does a declaration file do?

              Choose an answer to see the explanation.

            Key takeaways

            • Static type checking reads code before it runs. It turns many runtime crashes into warnings in your editor.
            • JSDoc plus checkJs adds types to .js files without changing how they run.
            • TypeScript adds annotations, inference, unions, narrowing, generics, and strict checks. Then the types are removed before the code runs.
            • unknown makes you check outside data before you use it. any turns checking off for that value.
            • Declaration files describe JavaScript that exists somewhere else. Runtime checks still protect data you don't control.

            Remember the one-liner.
            Use types to catch code mistakes early, and use runtime checks to make sure outside data is safe.

            Up next: Runtimes, the places where JavaScript runs.

            CompleteFrontend Clear concepts. Working examples.