TypeScript Fundamentals: Types, Generics and Real-World Patterns · Generics, Utility Types and Real-World TypeScript

Modules, tsconfig and Working with JavaScript

Organise code with ES modules and type-only imports, choose the tsconfig options that matter, validate objects with satisfies, and migrate a JavaScript project gradually.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A typed configuration module: a settings object checked with satisfies, a type derived from it, and a helper that reads values safely. You'll also see the tsconfig.json and module syntax a real project uses.

The idea

Modules. Every file with a top-level import or export is a module. Use named exports, and import type for imports that only carry types, so they are erased from the output:

// config.ts
export const defaults = { retries: 3 };
export type Config = typeof defaults;

// app.ts
import { defaults } from "./config.js";
import type { Config } from "./config.js";

With "module": "NodeNext" or "Node16" you write the .js extension in import paths, because that is the file Node.js will load.

tsconfig.json. The options that matter most:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["src"]
}

satisfies checks that a value matches a type without widening it. With const x = {...} satisfies Settings you keep exact literal types, and the compiler still catches missing or misspelt keys.

Using JavaScript and libraries. Packages ship types ("types" in package.json) or have community types in @types/.... A .d.ts file declares types for code written in JavaScript. To migrate gradually, set "allowJs": true and "checkJs": true, rename files to .ts one at a time, and add types where the benefit is highest.

The code

satisfies, derived types and safe config accessTry it yourself
type LogLevel = "debug" | "info" | "warn" | "error";

interface Settings {
  appName: string;
  port: number;
  logLevel: LogLevel;
  features: Record<string, boolean>;
}

// `satisfies` checks the shape but keeps the precise inferred type
const settings = {
  appName: "Inventory",
  port: 8080,
  logLevel: "info",
  features: { exports: true, beta: false },
} satisfies Settings;

// Because the literals were kept, we can derive exact types from the value
type FeatureName = keyof typeof settings.features;   // "exports" | "beta"
const level: "info" = settings.logLevel;             // would be an error if it were widened to LogLevel

function isEnabled(feature: FeatureName): boolean {
  return settings.features[feature];
}

console.log(settings.appName, settings.port, level);
console.log("exports:", isEnabled("exports"), "beta:", isEnabled("beta"));

// @ts-expect-error: "analytics" is not a declared feature
isEnabled("analytics");

// satisfies catches mistakes in the object itself
const broken = {
  appName: "Broken",
  // @ts-expect-error: port must be a number
  port: "8080",
  logLevel: "info",
  features: {},
} satisfies Settings;

// Reading from untyped input (for example JSON or process.env) requires narrowing
function readPort(raw: unknown, fallback: number): number {
  const value = typeof raw === "string" ? Number.parseInt(raw, 10) : raw;
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
}

console.log(readPort("3000", 8080), readPort("oops", 8080), readPort(undefined, 8080), broken.appName);

Output

Inventory 8080 info
exports: true beta: false
3000 8080 8080 Broken

Common mistakes

  • Mixing import and require without configuring the module system. Pick ES modules and set module and moduleResolution to match your runtime.
  • Leaving out import type under verbatimModuleSyntax. Type-only imports must be marked so bundlers can drop them.
  • Casting API responses with as. A cast is a promise you are not checking; parse and validate untrusted data (a schema library helps).
  • Turning on strict all at once in a large JavaScript codebase. Migrate directory by directory, or enable individual strict flags one at a time.
  • Editing node_modules/@types. Add your own declarations in a local .d.ts file instead.

Key points

  • Files with import or export are modules; use import type for type-only imports.
  • Start from strict: true and add noUncheckedIndexedAccess for safer indexing.
  • satisfies validates a value against a type without losing its exact literal types.
  • Migrate JavaScript gradually with allowJs, checkJs and .d.ts declarations.

Further reading: The Handbook: Modules and the tsconfig reference.

Practice questions

What does `satisfies` do that a type annotation does not?

Answer

A type annotation widens the value to the annotated type, losing precise literal information. satisfies checks that the value conforms to the type but keeps the narrower inferred type, so you still get exact keys and literals.

Why are API responses better validated than cast with `as`?

Answer

A cast only tells the compiler to trust you; if the data has a different shape, the error appears later at runtime. Validation checks the actual data and narrows it to a real type.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?