TypeScript in Practice

Real projects need typed browser APIs, a gradual path from JavaScript, and tools that check what the compiler cannot.

DOM manipulation

lib.dom.d.ts is generated from the web specifications (HTML DOM APIs). querySelector infers the element type from a tag name; for other selectors you pass it. Listeners get their event type from the event name. Lookups may return null and event.target is only an EventTarget, so narrow with instanceof (Narrowing):

DOM types inferred from selectors and event namesJavaScript
const form = document.querySelector("form");            // HTMLFormElement | null
const qty = document.querySelector<HTMLInputElement>("#qty");
const rows = document.querySelectorAll("tr");           // NodeListOf<HTMLTableRowElement>
form.addEventListener("submit", (event) => {            // event: SubmitEvent
  event.preventDefault();
  rows.forEach((row) => (row.hidden = row.rowIndex > (qty?.valueAsNumber ?? 0)));
});
document.addEventListener("click", ({ target }) => {    // target: EventTarget | null
  if (target instanceof HTMLButtonElement) target.disabled = true;
  target.disabled = false;
});
Output
dom.ts(4,1): error TS18047: 'form' is possibly 'null'.
dom.ts(10,3): error TS18047: 'target' is possibly 'null'.
dom.ts(10,10): error TS2339: Property 'disabled' does not exist on type 'EventTarget'.

querySelector<HTMLInputElement> is an unchecked assertion, like as: if #qty is a <select>, the types lie.

Migrating from JavaScript

Migrate in small, shippable steps; types never change runtime behavior, so the app keeps working.

A gradual JavaScript-to-TypeScript migration
Step Setting or action Result
1. Include JavaScript "allowJs": true, "noEmit": true tsc sees the whole project
2. Check JavaScript // @ts-check per file, then "checkJs": true Errors in .js, fixed with JSDoc (Declaration Files)
3. Rename leaf modules .js to .ts, utilities first Real annotations; // @ts-nocheck parks noisy files
4. Tighten "strict": true with "noImplicitAny": false, then remove it Full strictness, one flag at a time
5. Finish Add @types/*, drop allowJs An all-TypeScript project

Mark a known problem with // @ts-expect-error rather than // @ts-ignore: it fails once the line is fixed, so stale suppressions do not pile up.

Zod and type-fest

JSON from fetch or a form is really unknown. Zod 44,027 (https://github.com/colinhacks/zod 44,027 ) 4.6 (introduced in Essential JavaScript Libraries) validates such data and infers the static type from the schema, so you declare each shape once. type-fest 17,419 (https://github.com/sindresorhus/type-fest 17,419 ) 5.9 adds ready-made utility types such as SetRequired, PartialDeep, Simplify and Jsonify, all types-only:

One Zod schema gives a validator and a typeJavaScript
import * as z from "zod";
import type { SetRequired } from "type-fest";
const Order = z.object({ email: z.email(), qty: z.int().positive(), note: z.string().optional() });
type Order = z.infer<typeof Order>;             // { email: string; qty: number; note?: string }
export type GiftOrder = SetRequired<Order, "note">;   // note: string
const result = Order.safeParse(JSON.parse('{"email":"ada@example","qty":0}'));
if (result.success) console.log(result.data.qty); // result.data: Order
else console.log(z.prettifyError(result.error));
Output
✖ Invalid email address
  → at email
✖ Too small: expected number to be >0
  → at qty
typescript-eslint and testing types

typescript-eslint 16,401 (https://github.com/typescript-eslint/typescript-eslint 16,401 ) plugs TypeScript's parser and type checker into ESLint 39,810 (Code Quality Tools); recommendedTypeChecked enables rules that need type information, such as unhandled promises and unsafe any:

eslint.config.js with type-aware rulesJavaScript
import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig({
  files: ["**/*.ts"],
  extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],
  languageOptions: { parserOptions: { projectService: true } },
});

A call save(order); to an async function, without await, passes tsc but not the linter:

Output of 42
  5:3  error  Promises must be awaited, end with a call to .catch, end with a call to .then
    with a rejection handler or be explicitly marked as ignored with the `void` operator
      @typescript-eslint/no-floating-promises

Library authors also test the types themselves. expect-type 566 (https://github.com/mmkal/expect-type 566 ) turns type assertions into compile errors; Vitest 102,989 's expectTypeOf is built on it and checks *.test-d.ts files with vitest --typecheck:

Type tests that run under tscJavaScript
import { expectTypeOf } from "expect-type";
import * as z from "zod";
const Order = z.object({ email: z.email(), qty: z.int() });
expectTypeOf<z.infer<typeof Order>>().toEqualTypeOf<{ email: string; qty: number }>();
expectTypeOf(Order.parse).returns.toEqualTypeOf<{ email: string; qty: string }>();
// @ts-expect-error: qty must be a number
Order.parse satisfies (data: unknown) => { qty: string };
Output
types.test.ts(6,49): error TS2344: Type '{ email: string; qty: string; }' does not satisfy the
  constraint '{ email: string; qty: "Expected: string, Actual: number"; }'.