cf.completefrontendCode editorOpen lab
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
    Annotate JavaScriptUse JSDoc tags such as @param, @returns, @typedef, @property, @template, and @import.
  • 02
    Run a checker before runtimeEnable // @ts-check, checkJs, and strict TypeScript to catch editor squiggles before users see crashes.
  • 03
    Choose the right boundaryKnow when TypeScript, declaration files, generated .d.ts, and runtime validation each protect real code.

Catch type mistakes before users do

JavaScript is dynamically typed: values carry their type at runtime, and variables can hold different kinds of values over time. That flexibility is powerful, but it allows bugs such as a misspelled property, a missing callback, or undefined is not a function to wait until a user clicks the unlucky path.

Definition

Static type checking is a tool reading your code before it runs. It builds a model of values, parameters, properties, and return types, then reports type mistakes as editor squiggles or compiler diagnostics. It does not execute your program.

This lesson connects back to data types, checking types, and type coercion. Runtime JavaScript still uses typeof, Array.isArray, truthiness, and guards. The new tool is a checker that can warn before the runtime has a chance to fail.

Real-life analogyA proofreader before publication

A proofreader cannot guarantee that an article is true, but they can catch a missing word, mismatched name, or impossible sentence before readers see it. Static types play that early-warning role for values and calls.

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 owns the facts
In JavaScript: Runtime validation still checks outside data
In real life: A style guide makes review consistent
In JavaScript: JSDoc, TypeScript, and declarations give the checker shared rules

Where the analogy stops: A proofreader understands meaning better than a type checker. Types catch shape and flow mistakes; they do not prove business logic, accessibility, or security requirements.

Runtime bug: the checker would warn earlierPop out in the code editor (opens in a new tab)JavaScript
const lesson = {  title: "Types",  onComplete: undefined,}; lesson.onComplete();

A dynamic typing recap matters because it tells you where static types fit. They are not a replacement for functions, objects, linting, development environment setup, or transpilers and polyfills. They are one more feedback loop: fast, local, and focused on shapes.

Three safety nets that work together
ToolWhat it catchesWhat it cannot prove
JSDoc + checkJsKeep .js files and add comments. Great for gradual typing and libraries that do not want a build step.Comments can get verbose; advanced types are harder to read than TypeScript syntax.
TypeScript filesUse .ts/.tsx syntax, inference, strict mode, unions, generics, and rich editor tooling.Types are erased. You still compile, strip, or run with a compatible runtime.
Runtime validationUse real JavaScript checks at API, form, storage, and URL boundaries.It protects actual data, but it does not explain every internal call site while you type.

JSDoc: types in JavaScript comments

COMMENTS AS TYPES

JSDoc is the gentlest path for a JavaScript codebase. You keep .js files and add comments that TypeScript-powered editors understand. The comments are ignored by the JavaScript runtime, but // @ts-check or project-wide checkJs lets the checker use them.

JSDoc tags TypeScript understands in checked JavaScript
TagPurposeExample
@param {Type} nameTypes a function parameter.@param {number} price
@returns {Type}Types the value a function returns.@returns {string}
@type {Type}Types a variable, expression, or property./** @type {CourseCard} */
@typedef + @propertyNames object shapes for reuse.title, lessons, optional status
[name] or [name=default]Marks optional and default parameters or properties.@param {number} [limit=10]
A | BDescribes a union of possible types.string | number
@template TDefines a generic type variable.first<T>(items): T | undefined
@import {Type} from "./file"Imports a type name only inside JSDoc comments in TypeScript 5.5+.No runtime import is emitted.

Object shapes, optional values, defaults, and unions

The next example declares a reusable CourseCard object shape with @typedef and @property. Square brackets make status optional and document its default. The prefix parameter accepts a string | number union, so the function narrows it with typeof.

JSDoc object shape with an optional unionPop 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 the code line by line: the typedef names the object, each property adds a field, the function receives that named shape, and the body uses a normal runtime typeof check because unions describe possibilities, not decisions.

Generics with @template

Generic JSDoc lets one helper keep the type of the values it receives. Here @template T says “whatever item type the caller passes, use the same item type for the return.” The real inferred type displayed later is string | undefined, because arrays can be empty.

JSDoc generic first helperPop 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);

Importing types without importing runtime code

TypeScript 5.5 added a JSDoc @import tag. It brings type names into comments without creating a runtime import. You can still use the older inline form {import("./types").Course}, but @import reads more like normal imports when several annotations share a type.

JSDoc @import type-only commentPop out in the code editor (opens in a new tab)JavaScript
// @ts-check/** @import {CourseFromTypes} from "./types" */ /** @type {CourseFromTypes} */const importedCard = { title: "JSDoc", lessons: 2 }; console.log(importedCard.title);
Inferred types proved by the compiler
firstName on line 11string | undefined
importedCard on line 5CourseFromTypes
Try it yourself

Every type string here comes from TypeScript's checker in the test. The lesson stores no guessed inferred types.

The panel summarizes facts from the visible code blocks so the browser does not need to load TypeScript.

Turn on checkJs

REAL DIAGNOSTICS

A JSDoc comment becomes a safety net only when the checker is running. Put // @ts-check at the top of one file for a local opt-in, or set allowJs and checkJs in a jsconfig.json or tsconfig.json for the project. strict makes optional values, unknown, and nullish flows much more useful.

Project-wide JavaScript checkingJSON
{  "compilerOptions": {    "allowJs": true,    "checkJs": true,    "strict": true,    "noEmit": true  },  "include": ["src/**/*.js"]}
Type checker diagnostics without importing TypeScript in the browser
Checked source: optional callbackJavaScript
// @ts-check/** * @typedef {{ title: string, onComplete?: () => void }} Lesson */ /** @param {Lesson} lesson */function finishLesson(lesson) {  lesson.onComplete();} 
Compiler diagnosticsTypeScript 5.9
TS2722line 8

Cannot invoke an object which is possibly 'undefined'.

Try it yourself

Switch examples to see stored diagnostics. The browser only renders precomputed results; the lesson test re-runs TypeScript 5.9 and asserts each code, line, and message.

This is a checker-output playground, not a browser TypeScript compiler. Runnable JavaScript examples elsewhere still use Edit & run.

The important shift is timing. Without checking, the optional callback example waits until runtime to throw. With checking, TS2722 is an editor squiggle before you open the browser. With object shapes, the checker can also catch a property that looks plausible but does not exist.

TypeScript at a glance

TYPES ERASE

TypeScript is JavaScript plus type syntax and a compiler. You can annotate variables, parameters, returns, and object shapes; you also get inference, so many types are learned from values. A type alias can name any type expression, while an interface names an object shape and can be extended or merged. Teams often use both, choosing the clearer one for the job.

Unions, discriminants, in, and inferred result typePop 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);

The union says ApiState can be loading, ready, or error. The check state.status === "ready" narrows to the ready variant. The "message" in state branch narrows to the error variant. The lesson test asks TypeScript for the type of readyState and result, so the displayed inferred types are not guesses.

Generics

Let a function preserve relationships, such as input item type to output item type.

unknown vs any

unknown forces a guard; any turns the checker off for that value.

Strict mode

Enables checks that make optional, nullish, and implicit-any bugs visible.

unknown blocks unsafe property accessTypeScript
function readName(payload: unknown) {  return payload.name;} 
A plain TypeScript assignment errorTypeScript
type Course = { title: string; lessons: number };const course: Course = { title: "Types", lessons: "four" }; 
Types are erased at runtime

TypeScript does not add runtime checks. Browsers run JavaScript. This lesson site’s editor can run TypeScript examples because it strips the types before previewing them. Node 22.18+ and Node 23.6+ also strip erasable TypeScript syntax by default, but they do not type-check, do not read tsconfig paths, and syntax that needs generated JavaScript, such as enums, parameter properties, import aliases, or runtime namespaces, needs a transform path such as --experimental-transform-types or a full TypeScript runner.

The TC39 Type Annotations proposal is currently Stage 1. Its goal is similar in spirit to type stripping: JavaScript engines would treat compatible annotations as comments while external tools check them. It is a proposal, not a browser feature you can rely on today.

Narrowing meets real runtime data

STEP THROUGH

Type checkers narrow values by reading your code. Runtime validators narrow values by actually inspecting data. You need both at trust boundaries: fetch, JSON.parse, localStorage, URLs, forms, and messages from another window.

Runtime validator for unknown API data
Step 0 of 6Ready
Your turn: follow the blue line

Step through a real runtime validator. Static types help while coding; this guard protects the boundary where unknown data enters.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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");}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the JSON payload

Changing the payload starts a fresh replay. The guard is real JavaScript, so it protects runtime data.

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 ordinary JavaScript. In TypeScript you would annotate it as a type predicate, such as value is CoursePayload, so the checker learns from the same runtime branch that protects users.

Toy checker: read a call without running it
Step 0 of 5Ready
Your turn: follow the blue line

A tiny checker makes static checking concrete: it reads code-like facts and reports mismatches without executing the function being checked.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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.");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the call shape

This checker is intentionally tiny: it compares argument count and `typeof` only.

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 intentionally small. Real TypeScript builds a full program, reads declarations, follows imports, understands control flow, and compares rich types. The toy version still makes the core idea concrete: it can report a bad call without running the function being called.

Declaration files: types for JavaScript that already exists

.D.TS

A declaration file, ending in .d.ts, describes types without providing the implementation. It is how TypeScript understands many JavaScript packages, browser APIs, Node APIs, globals, and legacy scripts.

A .d.ts file for a legacy widget moduleTypeScript
declare module "legacy-widget" {  export type WidgetOptions = {    theme?: "light" | "dark";    onClose?: () => void;  };   export function mount(target: Element, options?: WidgetOptions): void;}
Declaration-file vocabulary
TermMeaningWhere you see it
.d.tsA file that describes types for JavaScript already supplied elsewhere.Ships beside a package or under @types/name.
declareSays a value, module, or global exists at runtime even though this file only describes it.declare module "legacy-widget" { ... }
types fieldPoints TypeScript at a package's declaration entry file."types": "./dist/index.d.ts"
exports fieldCan map type declarations per exported path."types": "./dist/index.d.ts" inside an export.
DefinitelyTypedCommunity declarations published as @types/* packages.npm i -D @types/node for Node globals.
JSDoc declaration emitTypeScript can read checked .js and emit .d.ts files.Use allowJs, checkJs, declaration, and emitDeclarationOnly.

Packages can point at declarations with a top-level types field, or with exports entries that include a types path for each public export. If a package does not ship declarations, DefinitelyTyped may publish a matching @types package.

Generating declarations from JSDoc

JSDoc is not only for editors. TypeScript can read checked JavaScript and emit declaration files for library consumers. The test for this lesson asks TypeScript to emit the declaration below from the lesson’s JSDoc source and compares it to the stored text.

Emit .d.ts from checked JavaScriptJSON
{  "compilerOptions": {    "allowJs": true,    "checkJs": true,    "declaration": true,    "emitDeclarationOnly": true,    "outDir": "types"  },  "include": ["src/**/*.js"]}
Generated declaration from the JSDoc sourceTypeScript
/** * @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;}; 

Practical workflow: choose the right feedback loop

SORT IT

In real teams, the answer is rarely “types only.” Use types for call shapes and object contracts, runtime validation for data you do not control, linting for consistency, and tests for behavior. Type systems are excellent at finding impossible operations; tests are excellent at proving important stories still work.

  • Start with // @ts-check on a risky JavaScript file.
  • Add JSDoc where inference cannot see enough, especially function boundaries.
  • Move to .ts when type syntax is clearer than comments.
  • Keep validators at API and user-input boundaries even after migration.
Caught by the type checker, runtime validation, or another tool?
  • `course.lenght` when `course` is typed as `{ length: number }`
  • `JSON.parse(response).lessons` from an API you do not control
  • `button.addEventListener("click", 42)`
  • A user submits an empty password string
  • A local variable is declared but never used
  • A sorted list uses ascending order when the product requires descending order
Try it yourself
0 of 6 correct

Sort each situation by the earliest reliable feedback loop. Some real bugs need tests or linting instead of type annotations.

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 just comments until // @ts-check or checkJs asks TypeScript to read it.
  • “TypeScript validates API data for me.” Types are erased. External data still needs runtime validation.
  • “any is a convenient fix.” It is sometimes necessary at a boundary, but it also hides mistakes. Prefer unknown until you narrow.
  • “A .d.ts file implements a module.” It only describes a module that must exist at runtime.
  • “Node stripping TypeScript means TypeScript is checked.” Type stripping removes erasable syntax; it does not run the compiler’s type analysis.
Similar safety tools, different jobs
QuestionStatic typesRuntime validationLint/tests
When does it run?Before runtime in an editor, compiler, or CI.While the program executes.Lint before runtime; tests execute selected behavior.
Best atCall shapes, properties, nullish values, and impossible operations.Untrusted data from APIs, users, storage, and URLs.Style, accessibility rules, behavior, regressions, and requirements.
Danger signSilencing with any or stale comments.Trusting typed code to validate unknown data.Testing only happy paths or linting without understanding.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a JSDoc-typed JavaScript output

Read the code and type the exact text printed by the final line.

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 optional-callback diagnostic

    Type the diagnostic code that catches the unsafe optional callback 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 · PracticeGeneric first output

      Type the name printed by the JSDoc generic example.

      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 · PracticeDiscriminated union output

        Predict the output from the TypeScript narrowing example.

        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 community declaration package prefix

          A JavaScript package does not ship its own types. What package prefix commonly supplies community declaration files?

          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 this checked JavaScript produce?

              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 config turns on checking for JavaScript files across a project?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does the narrowed TypeScript 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 prefer unknown over any for external data?

              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 job does a declaration file perform?

              Choose an answer to see the explanation.

            Key takeaways

            • Static type checking reads code before running it; it turns many runtime crashes into editor feedback.
            • JSDoc plus checkJs brings types to JavaScript files without changing runtime syntax.
            • TypeScript adds annotations, inference, unions, narrowing, generics, and strict checking, then erases types.
            • unknown keeps external data honest; any turns checking off for that value.
            • Declaration files describe JavaScript APIs, and runtime validation still protects untrusted data.

            Remember the one-liner.
            Use types to catch code-shape mistakes early, and use runtime checks to prove unknown data is safe.

            Up next: Runtimes.

            CompleteFrontend Clear concepts. Working examples.