# Compiler, modules and DOM boundaries

# A browser-oriented configuration

This tsconfig.json is for TypeScript 5.x with a bundler, not a Node CommonJS program. Install TypeScript locally in the example application; the TypeScript and Webpack configs describe different parts of the same build.

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "noEmitOnError": true,
    "sourceMap": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"]
}
Option Why it matters
target Chooses the JavaScript syntax level to emit
module / moduleResolution Preserve imports for the bundler and resolve them accordingly
lib Supplies type descriptions for language and browser APIs
strict Enables stronger checks, including nullability and implicit any checks
noEmitOnError Do not emit through tsc when there are type errors
sourceMap / outDir Debug original source; keep emitted files outside the source tree

lib describes APIs; it does not install them. A lower target can transform syntax but does not polyfill APIs such as fetch.

With TypeScript installed, these npm scripts expose its local CLI:

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "compile": "tsc"
  }
}

npm run typecheck checks without writing output; npm run compile emits separate JS files. Webpack will bundle them via its loader pipeline instead. Do not set permanent noEmit: true when using a loader that expects TypeScript to emit.

# Explicit modules and a safe DOM lookup

Use the Student class from Types and contracts in src/index.ts:

import { Student } from './Student';
import type { Greeter } from './Student';

const students: Greeter[] = [new Student('Ada', 1)];
const root = document.getElementById('root');

if (root === null) {
  throw new Error('Missing #root element');
}

root.textContent = students.map(student => student.greeting()).join('\n');

The HTML must contain an element with id="root", and the script must run after it exists. The null check both protects runtime execution and narrows the type. Writing document.getElementById('root')! only suppresses the compiler warning; it cannot make a missing node appear.

For a specific control, check element instanceof HTMLInputElement before reading value. Prefer textContent for data that should be displayed as text. .d.ts files describe types for code elsewhere; they do not supply runtime objects. Explicit exports/imports avoid accidentally creating global interfaces.

# JSON needs runtime validation

An API response does not become trustworthy because we write as StudentData[]. This standalone example checks the shape before returning typed values; its URL matches the Koa endpoint:

type StudentData = { id: number; name: string };

function isStudent(value: unknown): value is StudentData {
  if (typeof value !== 'object' || value === null) return false;
  const item = value as Record<string, unknown>;
  return typeof item.id === 'number' && Number.isFinite(item.id)
    && typeof item.name === 'string';
}

async function loadStudents(): Promise<StudentData[]> {
  const response = await fetch('/api/students');
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const data: unknown = await response.json();
  if (!Array.isArray(data) || !data.every(isStudent)) {
    throw new Error('Invalid student response');
  }
  return data;
}

The Record assertion lets us inspect properties; the actual field checks establish the contract. Callers still handle loading and errors as covered in Async HTTP. A shape check is not a substitute for application-specific validation or authorization.

Try it: remove the HTML target or change an API ID to a string. Which error can TypeScript detect, and which must be detected at runtime?

TSConfig reference (opens new window) · Strict null checks (opens new window)