diff --git a/src/typeChecker.ts b/src/typeChecker.ts index 7aed6219..11c159b2 100644 --- a/src/typeChecker.ts +++ b/src/typeChecker.ts @@ -236,8 +236,10 @@ export const isArrayOf: ( const k = `isArrayOf${typeName}`; return { typeGuard: { + // findIndex() visits holes as undefined, matching the diagnosis + // path; every() would skip them. [k]: (v: unknown): v is Array => - Array.isArray(v) && v.every((i) => isT(i)), + Array.isArray(v) && v.findIndex((i) => !isT(i)) < 0, }[k], *serialize(depth, done) { yield "Array<"; diff --git a/src/validate.test.ts b/src/validate.test.ts index 28dd99b2..c8de7f15 100644 --- a/src/validate.test.ts +++ b/src/validate.test.ts @@ -9,7 +9,7 @@ import { isOptionalOf, typeChecker, } from "./typeChecker.ts"; -import type { ValidationIssue } from "./types.ts"; +import type { TypeChecker, ValidationIssue } from "./types.ts"; import { validate, validateAll } from "./validate.ts"; import { ValidationIssueCode, @@ -406,3 +406,39 @@ test("exact object validation preserves its input without sanitizing it", () => assert.deepEqual(Object.getOwnPropertyDescriptors(input), before); assert.equal(Object.isFrozen(input), true); }); + +const assertArrayResult = ( + checker: TypeChecker>, + input: unknown, + expected: boolean, +): void => { + const label = `${checker} ${JSON.stringify(input)}`; + assert.equal(checker(input), expected, label); + assert.equal(checker.test(input) === null, expected, label); + assert.equal(validate(input, checker).ok, expected, label); + assert.equal(validateAll(input, checker).ok, expected, label); +}; + +test("array holes are checked like undefined elements on every path", () => { + const isStrings = isArrayOf(isString); + const isOptionalStrings = isArrayOf(isOptionalOf(isString)); + // biome-ignore lint/suspicious/noSparseArray: sparse arrays are the subject + const mixed = ["a", , "c"]; + const cases: Array<[unknown, boolean, boolean]> = [ + // [input, isStrings, isOptionalStrings] + [[], true, true], + [new Array(2), false, true], + [mixed, false, true], + [["a", undefined], false, true], + [["a", "b"], true, true], + ]; + for (const [input, strings, optionalStrings] of cases) { + assertArrayResult(isStrings, input, strings); + assertArrayResult(isOptionalStrings, input, optionalStrings); + } + const result = validate(mixed, isStrings); + assert.equal(result.ok, false); + if (!result.ok) { + assert.deepEqual(result.issue.path, [1]); + } +});