From 264370f76383ef5bc966948ee4c736c31a1a9075 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Wed, 7 Oct 2026 02:30:12 +0000 Subject: [PATCH] fix: mark in-bounds indexed reads non-null for noUncheckedIndexedAccess Consumers that compile the shipped src/ directory with noUncheckedIndexedAccess (for example, builds that transpile node_modules) fail on four indexed reads, starting with `'patch' is possibly 'undefined'` in applyPatches. The indices are bounded by the surrounding loop, the descriptor keys come from Reflect.ownKeys on the same object, and the last path element is present for every patch Immer generates, because the only empty path is a root replace, which applyPatches handles earlier. Assert these reads non-null with `!`, as the codebase already does elsewhere, rather than add runtime guards. Emitted JS is unchanged. Fixes #1143 --- __tests__/type-external.ts | 16 ++++++++++++++++ src/core/immerClass.ts | 2 +- src/plugins/patches.ts | 4 ++-- src/utils/common.ts | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/__tests__/type-external.ts b/__tests__/type-external.ts index fcf3b2df9..496a55571 100644 --- a/__tests__/type-external.ts +++ b/__tests__/type-external.ts @@ -1,6 +1,22 @@ import {isType, JSONArray, JSONObject, JSONTypes} from "type-plus" import {Draft} from "../src/types/types-external" import {createDraft, current, original} from "../src/immer" +import ts from "typescript" + +test("source compiles with noUncheckedIndexedAccess (#1143)", () => { + const {config, error} = ts.readConfigFile("tsconfig.json", ts.sys.readFile) + expect(error).toBeUndefined() + const parsed = ts.parseJsonConfigFileContent(config, ts.sys, ".", { + noUncheckedIndexedAccess: true, + skipLibCheck: true + }) + const program = ts.createProgram(parsed.fileNames, parsed.options) + expect( + [...parsed.errors, ...ts.getPreEmitDiagnostics(program)].map(diagnostic => + ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n") + ) + ).toEqual([]) +}) describe("Draft", () => { test("can use JSONTypes as T", () => { diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts index 2922f6121..224604b1f 100644 --- a/src/core/immerClass.ts +++ b/src/core/immerClass.ts @@ -207,7 +207,7 @@ export class Immer implements ProducersFns { // before applying patches let i: number for (i = patches.length - 1; i >= 0; i--) { - const patch = patches[i] + const patch = patches[i]! if (patch.path.length === 0 && patch.op === "replace") { base = patch.value break diff --git a/src/plugins/patches.ts b/src/plugins/patches.ts index 08c7d0f62..b3c86f86a 100644 --- a/src/plugins/patches.ts +++ b/src/plugins/patches.ts @@ -116,7 +116,7 @@ export function enablePatches() { function resolvePath(base: any, path: PatchPath): any { let current = base for (let i = 0; i < path.length - 1; i++) { - const key = path[i] + const key = path[i]! current = get(current, key) if (!isObjectish(current) || current === null) { throw new Error(`Cannot resolve path at '${path.join("/")}'`) @@ -351,7 +351,7 @@ export function enablePatches() { const type = getArchtype(base) const value = deepClonePatchValue(patch.value) // used to clone patch to ensure original patch is not modified, see #411 - const key = path[path.length - 1] + const key = path[path.length - 1]! switch (op) { case REPLACE: switch (type) { diff --git a/src/utils/common.ts b/src/utils/common.ts index 513c153e8..43f35ec0b 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -215,7 +215,7 @@ export function shallowCopy(base: any, strict: StrictMode) { let keys = Reflect.ownKeys(descriptors) for (let i = 0; i < keys.length; i++) { const key: any = keys[i] - const desc = descriptors[key] + const desc = descriptors[key]! if (desc[WRITABLE] === false) { desc[WRITABLE] = true desc[CONFIGURABLE] = true