diff --git a/src/typeChecker.test.ts b/src/typeChecker.test.ts index 393b04ae..95b10494 100644 --- a/src/typeChecker.test.ts +++ b/src/typeChecker.test.ts @@ -70,6 +70,64 @@ test("Should detect circular references.", () => { assert.throws(() => isT(obj), /^Error: CircularReference:/); }); +const assertSharedObjectAccepted = ( + isT: TypeChecker, + input: unknown, +): void => { + assert.equal(isT(input), true, `${isT}`); + assert.equal(validate(input, isT).ok, true, `${isT}`); + assert.equal(validateAll(input, isT).ok, true, `${isT}`); +}; + +test("Should accept objects shared between sibling properties.", () => { + const shared = { name: "Ada" }; + assertSharedObjectAccepted( + typeChecker({ left: { name: isString }, right: { name: isString } }), + { left: shared, right: shared }, + ); + assertSharedObjectAccepted( + isObjectWith({ left: { name: isString }, right: { name: isString } }), + { left: shared, right: shared }, + ); + assertSharedObjectAccepted( + typeChecker({ + left: { name: isString }, + right: isObjectWith({ name: /^A/ }), + }), + { left: shared, right: shared }, + ); + assertSharedObjectAccepted( + typeChecker({ + pair: { left: { name: isString } }, + right: { name: isString }, + }), + { pair: { left: shared }, right: shared }, + ); +}); + +test("Should detect circular references reached through a shared object.", () => { + interface Node { + name: string; + next: Node; + } + const isNode: TypeChecker = typeChecker( + { + name: isString, + get next() { + return isNode; + }, + }, + "Node", + ); + const isPair = typeChecker({ left: isNode, right: isNode }); + const shared = { name: "shared" } as Node; + shared.next = shared; + assert.throws( + () => isPair({ left: shared, right: shared }), + /^Error: CircularReference:/, + ); +}); + test("Should test type guards and object definitions.", () => { const isText = typeChecker( (input: unknown): input is string => typeof input === "string", diff --git a/src/typeChecker.ts b/src/typeChecker.ts index 7908657f..7aed6219 100644 --- a/src/typeChecker.ts +++ b/src/typeChecker.ts @@ -543,10 +543,16 @@ const objectFactoryProps = ( if (exact && hasUnknownKey(v)) { return false; } + // refs holds only the active recursion path, so an object shared by + // sibling properties is not mistaken for a circular reference. refs.set(v, typeName); - return properties().every(([k, is]) => - runGuard(is, getProperty(v, k), refs), - ); + try { + return properties().every(([k, is]) => + runGuard(is, getProperty(v, k), refs), + ); + } finally { + refs.delete(v); + } }; return { typeGuard,