Skip to content

MODELING DATA · LESSON 7 OF 24

Narrowing and user-defined type guards

Use runtime evidence to refine uncertain types.

  • typeof
  • in
  • instanceof
  • type predicates

Your runnable checkpoint

Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.

1. Understand the concepts

Narrowing follows control flow. typeof distinguishes primitive types, instanceof checks a constructor relationship, and in checks whether an object has a property. Remember typeof null is "object", so check null separately.

A predicate of the form value is Course tells the checker that a successful result establishes a type. The compiler trusts your predicate; you must actually check every required field. A predicate that always returns true is an unsafe promise.

unknown forces inspection before use. This is appropriate for parsed JSON and messages crossing a boundary. Unlike any, unknown does not silently permit arbitrary property access.

Here the predicate accepts extra fields but requires a finite numeric id and a string title. Real contracts may need additional constraints such as positive integer ids or a non-empty title.

2. Build and run

These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.

src/main.ts

src/main.ts
type Course = { id: number; title: string };
function isCourse(value: unknown): value is Course {
  return typeof value === "object" && value !== null
    && "id" in value && typeof value.id === "number" && Number.isFinite(value.id)
    && "title" in value && typeof value.title === "string";
}
function readTitle(value: unknown): string {
  if (isCourse(value)) return value.title.toUpperCase();
  return "Invalid course";
}
console.log(readTitle({ id: 1, title: "TypeScript" }));
console.log(readTitle(null));

package.json

package.json
{
  "name": "edupoly-typescript-narrowing-and-type-guards",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "check": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "devDependencies": {
    "typescript": "5.9.3"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true,
    "verbatimModuleSyntax": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "types": [],
    "moduleDetection": "force"
  },
  "include": [
    "src/**/*"
  ]
}

3. Trace the code and check the result

  1. The null check makes subsequent in checks safe.
  2. The predicate checks both required fields before returning true.
  3. The successful branch sees value as Course.

Expected result

TYPESCRIPT
Invalid course

4. Practice independently

Reject ids that are zero, negative or fractional.

Show the explanation

Add Number.isInteger(value.id) && value.id > 0 after the numeric check. Test zero, -1, 1.5 and 1.

When something goes wrong

If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.

Read the related official TypeScript documentation ↗