From 12340f9dcc0a16ea3578d5f0e3e15dc46a1007e0 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 8 Aug 2024 23:13:30 +0900 Subject: [PATCH 01/18] feat[inspect]: inspects record with symbol properties --- __snapshots__/_inspect_test.ts.snap | 18 ++++++++++++++++++ _inspect.ts | 5 ++--- _inspect_test.ts | 6 ++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/__snapshots__/_inspect_test.ts.snap b/__snapshots__/_inspect_test.ts.snap index 7b658a4..b92108b 100644 --- a/__snapshots__/_inspect_test.ts.snap +++ b/__snapshots__/_inspect_test.ts.snap @@ -30,6 +30,24 @@ snapshot[`inspect > record 3`] = ` snapshot[`inspect > record 4`] = `"{a: {b: {c: 0}}}"`; +snapshot[`inspect > record 5`] = `"{Symbol(a): 0}"`; + +snapshot[`inspect > record 6`] = ` +"{ + a: 0, + c: true, + Symbol(b): 1 +}" +`; + +snapshot[`inspect > record 7`] = ` +"{ + Symbol(a): { + Symbol(b): {Symbol(c): 0} + } +}" +`; + snapshot[`inspect > function 1`] = `"inspect"`; snapshot[`inspect > function 2`] = `"(anonymous)"`; diff --git a/_inspect.ts b/_inspect.ts index 010f31e..4e3af18 100644 --- a/_inspect.ts +++ b/_inspect.ts @@ -44,9 +44,8 @@ function inspectRecord( options: InspectOptions, ): string { const { threshold = defaultThreshold } = options; - const vs = Object.entries(value).map(([k, v]) => - `${k}: ${inspect(v, options)}` - ); + const vs = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)] + .map((k) => `${k.toString()}: ${inspect(value[k], options)}`); const s = vs.join(", "); if (s.length <= threshold) return `{${s}}`; const m = vs.join(",\n"); diff --git a/_inspect_test.ts b/_inspect_test.ts index c2f0042..897827e 100644 --- a/_inspect_test.ts +++ b/_inspect_test.ts @@ -25,6 +25,12 @@ Deno.test("inspect", async (t) => { await assertSnapshot(t, inspect({ a: 0, b: 1, c: 2 })); await assertSnapshot(t, inspect({ a: "a", b: 1, c: true })); await assertSnapshot(t, inspect({ a: { b: { c: 0 } } })); + await assertSnapshot(t, inspect({ [Symbol("a")]: 0 })); + await assertSnapshot(t, inspect({ a: 0, [Symbol("b")]: 1, c: true })); + await assertSnapshot( + t, + inspect({ [Symbol("a")]: { [Symbol("b")]: { [Symbol("c")]: 0 } } }), + ); }); await t.step("function", async (t) => { await assertSnapshot(t, inspect(inspect)); From 9af4a55c205e6d13bbe1fa943d690d92bebc686d Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 8 Aug 2024 19:46:07 +0900 Subject: [PATCH 02/18] feat[isObjectOf]: discover symbol properties in `predObj` Ref #94, #95 --- is/__snapshots__/object_of_test.ts.snap | 16 ++++ is/object_of.ts | 5 +- is/object_of_test.ts | 116 ++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/is/__snapshots__/object_of_test.ts.snap b/is/__snapshots__/object_of_test.ts.snap index 7524ffc..6f49853 100644 --- a/is/__snapshots__/object_of_test.ts.snap +++ b/is/__snapshots__/object_of_test.ts.snap @@ -17,3 +17,19 @@ snapshot[`isObjectOf > returns properly named predicate function 3`] = ` }) })" `; + +snapshot[`isObjectOf > with symbol properties > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + Symbol(s): isBoolean +})" +`; + +snapshot[`isObjectOf > with symbol properties > returns properly named predicate function 2`] = ` +"isObjectOf({ + Symbol(a): isObjectOf({ + Symbol(b): isObjectOf({Symbol(c): isBoolean}) + }) +})" +`; diff --git a/is/object_of.ts b/is/object_of.ts index 16defd6..91c0122 100644 --- a/is/object_of.ts +++ b/is/object_of.ts @@ -48,7 +48,10 @@ export function isObjectOf< Array.isArray(x) ) return false; // Check each values - return Object.keys(predObj).every((k) => predObj[k]((x as T)[k])); + return [ + ...Object.keys(predObj), + ...Object.getOwnPropertySymbols(predObj), + ].every((k) => predObj[k]((x as T)[k])); }, "isObjectOf", predObj, diff --git a/is/object_of_test.ts b/is/object_of_test.ts index 4aadc5c..514ef4b 100644 --- a/is/object_of_test.ts +++ b/is/object_of_test.ts @@ -152,4 +152,120 @@ Deno.test("isObjectOf", async (t) => { }); }, ); + + await t.step("with symbol properties", async (t) => { + const s = Symbol("s"); + const predObj = { + a: is.Number, + b: is.String, + [s]: is.Boolean, + }; + + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isObjectOf(predObj).name); + await assertSnapshot( + t, + isObjectOf({ + [Symbol("a")]: isObjectOf({ + [Symbol("b")]: isObjectOf({ [Symbol("c")]: is.Boolean }), + }), + }).name, + ); + }); + + await t.step("returns true on T object", () => { + assertEquals(isObjectOf(predObj)({ a: 0, b: "a", [s]: true }), true); + assertEquals( + isObjectOf(predObj)({ a: 0, b: "a", [s]: true, d: "ignored" }), + true, + "Undefined properties are ignored", + ); + assertEquals( + isObjectOf(predObj)({ + a: 0, + b: "a", + [s]: true, + [Symbol("t")]: "ignored", + }), + true, + "Undefined symbol properties are ignored", + ); + assertEquals( + isObjectOf(predObj)( + Object.assign(() => void 0, { a: 0, b: "a", [s]: true }), + ), + true, + "Function are treated as an object", + ); + }); + + await t.step("returns false on non T object", () => { + assertEquals(isObjectOf(predObj)("a"), false, "Value is not an object"); + assertEquals( + isObjectOf(predObj)({ a: 0, b: "a", [s]: "" }), + false, + "Object have a different type symbol property", + ); + assertEquals( + isObjectOf(predObj)({ a: 0, b: "a" }), + false, + "Object does not have symbol property", + ); + const arrayWithSymbolProp = ["ignored"]; + // deno-lint-ignore no-explicit-any + (arrayWithSymbolProp as any)[s] = true; + assertEquals( + isObjectOf({ [s]: is.Boolean })(arrayWithSymbolProp), + false, + "Value is not an object", + ); + }); + + await t.step("predicated type is correct", () => { + const a = Symbol("a"); + const b = Symbol("b"); + const c = Symbol("c"); + const d = Symbol("d"); + const e = Symbol("e"); + const f = Symbol("f"); + const predObj2 = { + [a]: as.Readonly(as.Optional(is.String)), + [b]: as.Optional(as.Readonly(is.String)), + [c]: as.Readonly(is.String), + [d]: as.Optional(is.String), + [e]: as.Unreadonly(as.Unoptional(as.Readonly(as.Optional(is.String)))), + [f]: as.Unoptional(as.Unreadonly(as.Optional(as.Readonly(is.String)))), + }; + const x: unknown = {}; + + if (isObjectOf(predObj)(x)) { + assertType< + Equal< + typeof x, + { + a: number; + b: string; + [s]: boolean; + } + > + >(true); + } + + if (isObjectOf(predObj2)(x)) { + assertType< + Equal< + typeof x, + { + readonly [a]?: string; + readonly [b]?: string; + readonly [c]: string; + [d]?: string; + [e]: string; + [f]: string; + } + > + >(true); + } + }); + }); }); From e32d7c8f40aa55c8e55280ad5c44792ca3f2ec6a Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 8 Aug 2024 22:38:26 +0900 Subject: [PATCH 03/18] feat[isOmitOf]: copy symbol properties from `pred.predObj` --- is/__snapshots__/omit_of_test.ts.snap | 9 +++++ is/omit_of.ts | 10 ++--- is/omit_of_test.ts | 53 +++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/is/__snapshots__/omit_of_test.ts.snap b/is/__snapshots__/omit_of_test.ts.snap index 19c8629..2aae083 100644 --- a/is/__snapshots__/omit_of_test.ts.snap +++ b/is/__snapshots__/omit_of_test.ts.snap @@ -8,3 +8,12 @@ snapshot[`isOmitOf > returns properly named predicate function 1`] = ` `; snapshot[`isOmitOf > returns properly named predicate function 2`] = `"isObjectOf({a: isNumber})"`; + +snapshot[`isOmitOf > with symbol properties > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + Symbol(c): isBoolean +})" +`; + +snapshot[`isOmitOf > with symbol properties > returns properly named predicate function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/omit_of.ts b/is/omit_of.ts index 6ef562d..242b355 100644 --- a/is/omit_of.ts +++ b/is/omit_of.ts @@ -43,11 +43,11 @@ export function isOmitOf< ): & Predicate>> & IsPredObj

{ - const s = new Set(keys); - const predObj = Object.fromEntries( - Object.entries(pred.predObj).filter(([k]) => !s.has(k as K)), - ); - return isObjectOf(predObj) as + const predObj = { ...pred.predObj }; + for (const key of keys) { + delete predObj[key]; + } + return isObjectOf(predObj as Record>) as & Predicate>> & IsPredObj

; } diff --git a/is/omit_of_test.ts b/is/omit_of_test.ts index 107d4f2..01d7935 100644 --- a/is/omit_of_test.ts +++ b/is/omit_of_test.ts @@ -54,4 +54,57 @@ Deno.test("isOmitOf", async (t) => { >(true); } }); + + await t.step("with symbol properties", async (t) => { + const b = Symbol("b"); + const c = Symbol("c"); + const pred = is.ObjectOf({ + a: is.Number, + [b]: is.String, + [c]: is.Boolean, + }); + + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isOmitOf(pred, [b]), "function"); + await assertSnapshot(t, isOmitOf(pred, [b]).name); + await assertSnapshot(t, isOmitOf(isOmitOf(pred, [b]), [c]).name); + }); + + await t.step("returns true on Omit object", () => { + assertEquals( + isOmitOf(pred, [b])({ a: 0, [b]: undefined, [c]: true }), + true, + ); + assertEquals(isOmitOf(pred, [b, c])({ a: 0 }), true); + }); + + await t.step("returns false on non Omit object", () => { + assertEquals( + isOmitOf(pred, [b])("a"), + false, + "Value is not an object", + ); + assertEquals( + isOmitOf(pred, [b])({ a: 0, [b]: "a", [c]: "" }), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const x: unknown = { a: 0, [b]: "a", [c]: true }; + + if (isOmitOf(pred, [b])(x)) { + assertType< + Equal + >(true); + } + + if (isOmitOf(isOmitOf(pred, [b]), [c])(x)) { + assertType< + Equal + >(true); + } + }); + }); }); From c96ee8f647f8f6d2437d6133e67138b809e2042e Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 01:52:07 +0900 Subject: [PATCH 04/18] feat[isPickOf]: copy symbol properties from `pred.predObj` --- is/__snapshots__/pick_of_test.ts.snap | 9 +++++ is/pick_of.ts | 14 +++++--- is/pick_of_test.ts | 50 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/is/__snapshots__/pick_of_test.ts.snap b/is/__snapshots__/pick_of_test.ts.snap index 911dc0e..2be11a2 100644 --- a/is/__snapshots__/pick_of_test.ts.snap +++ b/is/__snapshots__/pick_of_test.ts.snap @@ -8,3 +8,12 @@ snapshot[`isPickOf > returns properly named predicate function 1`] = ` `; snapshot[`isPickOf > returns properly named predicate function 2`] = `"isObjectOf({a: isNumber})"`; + +snapshot[`isPickOf > with symbol properties > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + Symbol(c): isBoolean +})" +`; + +snapshot[`isPickOf > with symbol properties > returns properly named predicate function 2`] = `"isObjectOf({Symbol(c): isBoolean})"`; diff --git a/is/pick_of.ts b/is/pick_of.ts index e605ceb..3ac46c4 100644 --- a/is/pick_of.ts +++ b/is/pick_of.ts @@ -43,11 +43,15 @@ export function isPickOf< ): & Predicate>> & IsPredObj

{ - const s = new Set(keys); - const predObj = Object.fromEntries( - Object.entries(pred.predObj).filter(([k]) => s.has(k as K)), - ); - return isObjectOf(predObj) as + const omitKeys = new Set([ + ...Object.keys(pred.predObj), + ...Object.getOwnPropertySymbols(pred.predObj), + ]).difference(new Set(keys)); + const predObj = { ...pred.predObj }; + for (const key of omitKeys) { + delete predObj[key]; + } + return isObjectOf(predObj as Record>) as & Predicate>> & IsPredObj

; } diff --git a/is/pick_of_test.ts b/is/pick_of_test.ts index 9f79564..238a617 100644 --- a/is/pick_of_test.ts +++ b/is/pick_of_test.ts @@ -51,4 +51,54 @@ Deno.test("isPickOf", async (t) => { >(true); } }); + + await t.step("with symbol properties", async (t) => { + const b = Symbol("b"); + const c = Symbol("c"); + const pred = is.ObjectOf({ + a: is.Number, + [b]: is.String, + [c]: is.Boolean, + }); + + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isPickOf(pred, ["a", c]).name); + await assertSnapshot(t, isPickOf(isPickOf(pred, ["a", c]), [c]).name); + }); + + await t.step("returns true on Pick object", () => { + assertEquals( + isPickOf(pred, ["a", c])({ a: 0, [b]: undefined, [c]: true }), + true, + ); + assertEquals(isPickOf(pred, ["a"])({ a: 0 }), true); + }); + + await t.step("returns false on non Pick object", () => { + assertEquals( + isPickOf(pred, ["a", c])("a"), + false, + "Value is not an object", + ); + assertEquals( + isPickOf(pred, ["a", c])({ a: 0, [b]: "a", [c]: "" }), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, [b]: "a", [c]: true }; + if (isPickOf(pred, ["a", c])(a)) { + assertType< + Equal + >(true); + } + if (isPickOf(isPickOf(pred, ["a", c]), ["a"])(a)) { + assertType< + Equal + >(true); + } + }); + }); }); From 457defe85c4355c304c2bef61b912b61f18b1ea5 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 02:34:13 +0900 Subject: [PATCH 05/18] feat[isPartialOf]: discover symbol properties in `pred.predObj` --- is/__snapshots__/partial_of_test.ts.snap | 24 +++++++++++ is/partial_of.ts | 11 +++-- is/partial_of_test.ts | 51 ++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/is/__snapshots__/partial_of_test.ts.snap b/is/__snapshots__/partial_of_test.ts.snap index 6f96fc2..7e9cb3f 100644 --- a/is/__snapshots__/partial_of_test.ts.snap +++ b/is/__snapshots__/partial_of_test.ts.snap @@ -23,3 +23,27 @@ snapshot[`isPartialOf > returns properly named predicate function 2`] = ` d: asOptional(asReadonly(isString)) })" `; + +snapshot[`isPartialOf > with symbol properties > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: asOptional(isNumber), + Symbol(b): asOptional(isUnionOf([ + isString, + isUndefined + ])), + Symbol(c): asOptional(isBoolean), + Symbol(c): asOptional(asReadonly(isString)) +})" +`; + +snapshot[`isPartialOf > with symbol properties > returns properly named predicate function 2`] = ` +"isObjectOf({ + a: asOptional(isNumber), + Symbol(b): asOptional(isUnionOf([ + isString, + isUndefined + ])), + Symbol(c): asOptional(isBoolean), + Symbol(c): asOptional(asReadonly(isString)) +})" +`; diff --git a/is/partial_of.ts b/is/partial_of.ts index 440db32..56e1395 100644 --- a/is/partial_of.ts +++ b/is/partial_of.ts @@ -42,9 +42,14 @@ export function isPartialOf< ): & Predicate>> & IsPredObj

{ - const predObj = Object.fromEntries( - Object.entries(pred.predObj).map(([k, v]) => [k, asOptional(v)]), - ) as Record>; + const keys = [ + ...Object.keys(pred.predObj), + ...Object.getOwnPropertySymbols(pred.predObj), + ]; + const predObj: Record> = { ...pred.predObj }; + for (const key of keys) { + predObj[key] = asOptional(predObj[key]); + } return isObjectOf(predObj) as & Predicate>> & IsPredObj

; diff --git a/is/partial_of_test.ts b/is/partial_of_test.ts index de0a584..e0433a9 100644 --- a/is/partial_of_test.ts +++ b/is/partial_of_test.ts @@ -47,4 +47,55 @@ Deno.test("isPartialOf", async (t) => { >(true); } }); + + await t.step("with symbol properties", async (t) => { + const b = Symbol("b"); + const c = Symbol("c"); + const d = Symbol("c"); + const pred = is.ObjectOf({ + a: is.Number, + [b]: is.UnionOf([is.String, is.Undefined]), + [c]: as.Optional(is.Boolean), + [d]: as.Readonly(is.String), + }); + + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isPartialOf(pred).name); + await assertSnapshot(t, isPartialOf(isPartialOf(pred)).name); + }); + + await t.step("returns true on Partial object", () => { + assertEquals( + isPartialOf(pred)({ a: undefined, [b]: undefined, [c]: undefined }), + true, + ); + assertEquals(isPartialOf(pred)({}), true); + }); + + await t.step("returns false on non Partial object", () => { + assertEquals(isPartialOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isPartialOf(pred)({ a: 0, [b]: "a", [c]: "" }), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, [b]: "a", [c]: true }; + if (isPartialOf(pred)(a)) { + assertType< + Equal< + typeof a, + Partial<{ + a: number; + [b]: string; + [c]: boolean; + readonly [d]: string; + }> + > + >(true); + } + }); + }); }); From e0633791a530924d19a3922e60a76508bebc8364 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 02:43:01 +0900 Subject: [PATCH 06/18] test[isIntersectionOf]: discover symbol properties in annotated `predObj` --- is/__snapshots__/intersection_of_test.ts.snap | 16 ++++ is/intersection_of_test.ts | 75 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/is/__snapshots__/intersection_of_test.ts.snap b/is/__snapshots__/intersection_of_test.ts.snap index 56278e4..6e36920 100644 --- a/is/__snapshots__/intersection_of_test.ts.snap +++ b/is/__snapshots__/intersection_of_test.ts.snap @@ -15,3 +15,19 @@ snapshot[`isIntersectionOf > returns properly named predicate function 3`] = isObjectOf({b: isString}) ])" `; + +snapshot[`isIntersectionOf > with symbol properties > returns properly named predicate function 1`] = `"isString"`; + +snapshot[`isIntersectionOf > with symbol properties > returns properly named predicate function 2`] = ` +"isObjectOf({ + a: isNumber, + Symbol(b): isString +})" +`; + +snapshot[`isIntersectionOf > with symbol properties > returns properly named predicate function 3`] = ` +"isIntersectionOf([ + isFunction, + isObjectOf({Symbol(b): isString}) +])" +`; diff --git a/is/intersection_of_test.ts b/is/intersection_of_test.ts index a22c4f7..c0f474b 100644 --- a/is/intersection_of_test.ts +++ b/is/intersection_of_test.ts @@ -77,4 +77,79 @@ Deno.test("isIntersectionOf", async (t) => { >(true); } }); + + await t.step("with symbol properties", async (t) => { + const b = Symbol("b"); + const objPreds = [ + is.ObjectOf({ a: is.Number }), + is.ObjectOf({ [b]: is.String }), + ] as const; + const mixPreds = [ + is.Function, + is.ObjectOf({ [b]: is.String }), + ] as const; + + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isIntersectionOf([is.String]), "function"); + await assertSnapshot(t, isIntersectionOf([is.String]).name); + await assertSnapshot(t, isIntersectionOf(objPreds).name); + await assertSnapshot(t, isIntersectionOf(mixPreds).name); + }); + + await t.step("returns true on all of T", () => { + const f = Object.assign(() => void 0, { [b]: "a" }); + assertEquals(isIntersectionOf([is.String])("a"), true); + assertEquals(isIntersectionOf(objPreds)({ a: 0, [b]: "a" }), true); + assertEquals(isIntersectionOf(mixPreds)(f), true); + }); + + await t.step("returns false on non of T", () => { + const f = Object.assign(() => void 0, { [b]: "a" }); + assertEquals(isIntersectionOf(objPreds)("a"), false); + assertEquals(isIntersectionOf(mixPreds)({ a: 0, [b]: "a" }), false); + assertEquals(isIntersectionOf([is.String])(f), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = undefined; + + if (isIntersectionOf([is.String])(a)) { + assertType>(true); + } + + if (isIntersectionOf(objPreds)(a)) { + assertType>(true); + } + + if (isIntersectionOf(mixPreds)(a)) { + assertType< + Equal< + typeof a, + & ((...args: unknown[]) => unknown) + & { [b]: string } + > + >(true); + } + }); + + await t.step("predicated type is correct (#68)", () => { + const a: unknown = undefined; + const pred = isIntersectionOf([ + is.ObjectOf({ id: is.String }), + is.UnionOf([ + is.ObjectOf({ result: is.String }), + is.ObjectOf({ error: is.String }), + ]), + ]); + + if (pred(a)) { + assertType< + Equal< + typeof a, + { id: string } & ({ result: string } | { error: string }) + > + >(true); + } + }); + }); }); From f53e513fc6455a0672250c4861ac0e18484c4e12 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 8 Aug 2024 20:04:09 +0900 Subject: [PATCH 07/18] feat[isStrictOf]: discover symbol properties in `pred.predObj` --- is/__snapshots__/strict_of_test.ts.snap | 16 ++++ is/strict_of.ts | 11 ++- is/strict_of_test.ts | 108 ++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 4 deletions(-) diff --git a/is/__snapshots__/strict_of_test.ts.snap b/is/__snapshots__/strict_of_test.ts.snap index add9027..3af0784 100644 --- a/is/__snapshots__/strict_of_test.ts.snap +++ b/is/__snapshots__/strict_of_test.ts.snap @@ -17,3 +17,19 @@ snapshot[`isStrictOf > returns properly named predicate function 3`] = ` })) }))" `; + +snapshot[`isStrictOf > with symbol properties > returns properly named predicate function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + Symbol(b): isString, + Symbol(c): isBoolean +}))" +`; + +snapshot[`isStrictOf > with symbol properties > returns properly named predicate function 2`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + Symbol(b): isStrictOf(isObjectOf({Symbol(c): isBoolean})) + })) +}))" +`; diff --git a/is/strict_of.ts b/is/strict_of.ts index 0181c2d..a3f50c3 100644 --- a/is/strict_of.ts +++ b/is/strict_of.ts @@ -40,15 +40,18 @@ export function isStrictOf< ): & Predicate & IsPredObj

{ - const s = new Set(Object.keys(pred.predObj)); + const s = new Set(getKeys(pred.predObj)); return rewriteName( (x: unknown): x is T => { if (!pred(x)) return false; - // deno-lint-ignore no-explicit-any - const ks = Object.keys(x as any); - return ks.length <= s.size && ks.every((k) => s.has(k)); + const ks = new Set(getKeys(x)); + return ks.difference(s).size === 0; }, "isStrictOf", pred, ) as Predicate & IsPredObj

; } + +function getKeys(o: Record) { + return [...Object.keys(o), ...Object.getOwnPropertySymbols(o)]; +} diff --git a/is/strict_of_test.ts b/is/strict_of_test.ts index 2b93f5a..97cbeca 100644 --- a/is/strict_of_test.ts +++ b/is/strict_of_test.ts @@ -155,4 +155,112 @@ Deno.test("isStrictOf", async (t) => { } }); }); + + await t.step("with symbol properties", async (t) => { + const b = Symbol("b"); + const c = Symbol("c"); + const predObj = { + a: is.Number, + [b]: is.UnionOf([is.String, is.Undefined]), + [c]: as.Optional(is.Boolean), + }; + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot( + t, + isStrictOf( + is.ObjectOf({ a: is.Number, [b]: is.String, [c]: is.Boolean }), + ) + .name, + ); + await assertSnapshot( + t, + isStrictOf( + is.ObjectOf({ + a: isStrictOf( + is.ObjectOf({ + [b]: isStrictOf(is.ObjectOf({ [c]: is.Boolean })), + }), + ), + }), + ).name, + ); + }); + + await t.step("returns true on T object", () => { + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, [b]: "a", [c]: true }), + true, + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, [b]: "a" }), + true, + "Object does not have an optional property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, [b]: "a", [c]: undefined }), + true, + "Object has `undefined` as value of optional property", + ); + }); + + await t.step("returns false on non T object", () => { + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, [b]: "a", [c]: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, [b]: "a", [c]: null }), + false, + "Object has `null` as value of optional property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + [b]: "a", + [c]: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + [b]: "a", + [c]: true, + [Symbol("d")]: "invalid", + }), + false, + "Object have an unknown symbol property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + [b]: "a", + d: "invalid", + }), + false, + "Object have the same number of properties but an unknown property exists", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + [b]: "a", + [Symbol("d")]: "invalid", + }), + false, + "Object have the same number of properties but an unknown symbol property exists", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, [b]: "a" }; + if (isStrictOf(is.ObjectOf(predObj))(a)) { + assertType< + Equal + >(true); + } + }); + }); }); From 35c5d1a1d2e92696ed73efcf71962d855c23eac7 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 03:23:54 +0900 Subject: [PATCH 08/18] test[isReadonlyOf]: test with isObjectOf with symbol properties --- is/__snapshots__/readonly_of_test.ts.snap | 22 +++++++++++ is/readonly_of_test.ts | 48 +++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/is/__snapshots__/readonly_of_test.ts.snap b/is/__snapshots__/readonly_of_test.ts.snap index 308a72d..652409e 100644 --- a/is/__snapshots__/readonly_of_test.ts.snap +++ b/is/__snapshots__/readonly_of_test.ts.snap @@ -45,3 +45,25 @@ snapshot[`isReadonlyOf > with isTupleOf > returns properly named predicate fu snapshot[`isReadonlyOf > with isUniformTupleOf > returns properly named predicate function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; snapshot[`isReadonlyOf > with isUniformTupleOf > returns properly named predicate function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; + +snapshot[`isReadonlyOf > with symbol properties > with isObjectOf > returns properly named predicate function 1`] = ` +"isReadonlyOf(isObjectOf({ + a: isNumber, + Symbol(b): isUnionOf([ + isString, + isUndefined + ]), + Symbol(c): asReadonly(isBoolean) +}))" +`; + +snapshot[`isReadonlyOf > with symbol properties > with isObjectOf > returns properly named predicate function 2`] = ` +"isReadonlyOf(isObjectOf({ + a: isNumber, + Symbol(b): isUnionOf([ + isString, + isUndefined + ]), + Symbol(c): asReadonly(isBoolean) +}))" +`; diff --git a/is/readonly_of_test.ts b/is/readonly_of_test.ts index 0661542..007dcfa 100644 --- a/is/readonly_of_test.ts +++ b/is/readonly_of_test.ts @@ -170,4 +170,52 @@ Deno.test("isReadonlyOf", async (t) => { } }); }); + + await t.step("with symbol properties", async (t) => { + await t.step("with isObjectOf", async (t) => { + const b = Symbol("b"); + const c = Symbol("c"); + const pred = is.ObjectOf({ + a: is.Number, + [b]: is.UnionOf([is.String, is.Undefined]), + [c]: as.Readonly(is.Boolean), + }); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isReadonlyOf(pred).name); + await assertSnapshot(t, isReadonlyOf(isReadonlyOf(pred)).name); + }); + + await t.step("returns true on Readonly object", () => { + assertEquals( + isReadonlyOf(pred)({ a: 0, [b]: "b", [c]: true } as const), + true, + ); + assertEquals( + isReadonlyOf(pred)({ a: 0, [b]: "b", [c]: true }), + true, + ); + }); + + await t.step("returns false on non Readonly object", () => { + assertEquals(isReadonlyOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isReadonlyOf(pred)({ a: 0, [b]: "a", [c]: "" }), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, [b]: "a", [c]: true }; + if (isReadonlyOf(pred)(a)) { + assertType< + Equal< + typeof a, + Readonly<{ a: number; [b]: string | undefined; [c]: boolean }> + > + >(true); + } + }); + }); + }); }); From 0d607880119efbca374ca3b6e669c315d061f133 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 02:52:50 +0900 Subject: [PATCH 09/18] feat[isRequiredOf]: discover symbol properties in `pred.predObj` --- is/__snapshots__/required_of_test.ts.snap | 24 ++++++++++ is/required_of.ts | 11 +++-- is/required_of_test.ts | 55 +++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/is/__snapshots__/required_of_test.ts.snap b/is/__snapshots__/required_of_test.ts.snap index 7c30fac..2bb6a96 100644 --- a/is/__snapshots__/required_of_test.ts.snap +++ b/is/__snapshots__/required_of_test.ts.snap @@ -23,3 +23,27 @@ snapshot[`isRequiredOf > returns properly named predicate function 2`] = ` d: asReadonly(isString) })" `; + +snapshot[`isRequiredOf > with symbol properties > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + Symbol(b): isUnionOf([ + isString, + isUndefined + ]), + Symbol(c): isBoolean, + Symbol(d): asReadonly(isString) +})" +`; + +snapshot[`isRequiredOf > with symbol properties > returns properly named predicate function 2`] = ` +"isObjectOf({ + a: isNumber, + Symbol(b): isUnionOf([ + isString, + isUndefined + ]), + Symbol(c): isBoolean, + Symbol(d): asReadonly(isString) +})" +`; diff --git a/is/required_of.ts b/is/required_of.ts index 3b7fba7..310ba2a 100644 --- a/is/required_of.ts +++ b/is/required_of.ts @@ -42,9 +42,14 @@ export function isRequiredOf< ): & Predicate>> & IsPredObj

{ - const predObj = Object.fromEntries( - Object.entries(pred.predObj).map(([k, v]) => [k, asUnoptional(v)]), - ); + const keys = [ + ...Object.keys(pred.predObj), + ...Object.getOwnPropertySymbols(pred.predObj), + ]; + const predObj: Record> = { ...pred.predObj }; + for (const key of keys) { + predObj[key] = asUnoptional(predObj[key]); + } return isObjectOf(predObj) as & Predicate>> & IsPredObj

; diff --git a/is/required_of_test.ts b/is/required_of_test.ts index 16c00e2..3f638b4 100644 --- a/is/required_of_test.ts +++ b/is/required_of_test.ts @@ -51,4 +51,59 @@ Deno.test("isRequiredOf", async (t) => { >(true); } }); + + await t.step("with symbol properties", async (t) => { + const b = Symbol("b"); + const c = Symbol("c"); + const d = Symbol("d"); + const pred = is.ObjectOf({ + a: is.Number, + [b]: is.UnionOf([is.String, is.Undefined]), + [c]: as.Optional(is.Boolean), + [d]: as.Readonly(is.String), + }); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRequiredOf(pred).name); + await assertSnapshot(t, isRequiredOf(isRequiredOf(pred)).name); + }); + + await t.step("returns true on Required object", () => { + assertEquals( + isRequiredOf(pred)({ a: undefined, [b]: undefined, [c]: undefined }), + false, + "Object does not have required properties", + ); + assertEquals( + isRequiredOf(pred)({}), + false, + "Object does not have required properties", + ); + }); + + await t.step("returns false on non Required object", () => { + assertEquals(isRequiredOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isRequiredOf(pred)({ a: 0, [b]: "a", [c]: "" }), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, [b]: "a", [c]: true }; + if (isRequiredOf(pred)(a)) { + assertType< + Equal< + typeof a, + { + a: number; + [b]: string | undefined; + [c]: boolean; + readonly [d]: string; + } + > + >(true); + } + }); + }); }); From 5b07537dee689bb2ddca6369ed521b808fa96ab1 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 15:54:54 +0900 Subject: [PATCH 10/18] fix[isRecordObjectOf]: checks only own properties --- is/record_object_of.ts | 3 ++- is/record_object_of_test.ts | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/is/record_object_of.ts b/is/record_object_of.ts index dbd05f8..13a7465 100644 --- a/is/record_object_of.ts +++ b/is/record_object_of.ts @@ -39,7 +39,8 @@ export function isRecordObjectOf( return rewriteName( (x: unknown): x is Record => { if (!isRecordObject(x)) return false; - for (const k in x) { + const keys = Object.keys(x); + for (const k of keys) { if (!pred(x[k])) return false; if (predKey && !predKey(k)) return false; } diff --git a/is/record_object_of_test.ts b/is/record_object_of_test.ts index 596ea05..48675fe 100644 --- a/is/record_object_of_test.ts +++ b/is/record_object_of_test.ts @@ -29,6 +29,29 @@ Deno.test("isRecordObjectOf", async (t) => { assertType>>(true); } }); + + await t.step("checks only object's own properties", async (t) => { + await t.step("returns true on T record", () => { + assertEquals( + isRecordObjectOf(is.Number)( + Object.assign(Object.create({ p: "ignore" }), { a: 0 }), + ), + true, + ); + assertEquals( + isRecordObjectOf(is.String)( + Object.assign(Object.create({ p: 0 /* ignore */ }), { a: "a" }), + ), + true, + ); + assertEquals( + isRecordObjectOf(is.Boolean)( + Object.assign(Object.create({ p: "ignore" }), { a: true }), + ), + true, + ); + }); + }); }); Deno.test("isRecordObjectOf", async (t) => { @@ -64,4 +87,34 @@ Deno.test("isRecordObjectOf", async (t) => { assertType>>(true); } }); + + await t.step("checks only object's own properties", async (t) => { + await t.step("returns true on T record", () => { + assertEquals( + isRecordObjectOf(is.Number, is.String)( + Object.assign(Object.create({ p: "ignore" }), { a: 0 }), + ), + true, + ); + assertEquals( + isRecordObjectOf(is.String, is.String)( + Object.assign(Object.create({ p: 0 /* ignore */ }), { a: "a" }), + ), + true, + ); + assertEquals( + isRecordObjectOf(is.Boolean, is.String)( + Object.assign(Object.create({ p: "ignore" }), { a: true }), + ), + true, + ); + assertEquals( + isRecordObjectOf(is.String, is.Number)( + Object.assign(Object.create({ p: "ignore" }), {/* empty */}), + ), + true, + "No own properties", + ); + }); + }); }); From 1aafdc94d2119d897091d5e682fcc38609e4135e Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 16:27:45 +0900 Subject: [PATCH 11/18] feat[isRecordObjectOf]: checks own symbol key properties --- .../record_object_of_test.ts.snap | 4 ++ is/record_object_of.ts | 5 +- is/record_object_of_test.ts | 68 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/is/__snapshots__/record_object_of_test.ts.snap b/is/__snapshots__/record_object_of_test.ts.snap index 1254e9f..b78c694 100644 --- a/is/__snapshots__/record_object_of_test.ts.snap +++ b/is/__snapshots__/record_object_of_test.ts.snap @@ -7,3 +7,7 @@ snapshot[`isRecordObjectOf > returns properly named predicate function 2`] = snapshot[`isRecordObjectOf > returns properly named predicate function 1`] = `"isRecordObjectOf(isNumber, isString)"`; snapshot[`isRecordObjectOf > returns properly named predicate function 2`] = `"isRecordObjectOf((anonymous), isString)"`; + +snapshot[`isRecordObjectOf > with symbol properties > returns properly named predicate function 1`] = `"isRecordObjectOf(isNumber, isSymbol)"`; + +snapshot[`isRecordObjectOf > with symbol properties > returns properly named predicate function 2`] = `"isRecordObjectOf((anonymous), isSymbol)"`; diff --git a/is/record_object_of.ts b/is/record_object_of.ts index 13a7465..46d22a2 100644 --- a/is/record_object_of.ts +++ b/is/record_object_of.ts @@ -39,7 +39,10 @@ export function isRecordObjectOf( return rewriteName( (x: unknown): x is Record => { if (!isRecordObject(x)) return false; - const keys = Object.keys(x); + const keys = [ + ...Object.keys(x), + ...Object.getOwnPropertySymbols(x), + ]; for (const k of keys) { if (!pred(x[k])) return false; if (predKey && !predKey(k)) return false; diff --git a/is/record_object_of_test.ts b/is/record_object_of_test.ts index 48675fe..7ece23f 100644 --- a/is/record_object_of_test.ts +++ b/is/record_object_of_test.ts @@ -52,6 +52,21 @@ Deno.test("isRecordObjectOf", async (t) => { ); }); }); + + await t.step("with symbol properties", async (t) => { + const a = Symbol("a"); + await t.step("returns true on T record", () => { + assertEquals(isRecordObjectOf(is.Number)({ [a]: 0 }), true); + assertEquals(isRecordObjectOf(is.String)({ [a]: "a" }), true); + assertEquals(isRecordObjectOf(is.Boolean)({ [a]: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordObjectOf(is.String)({ [a]: 0 }), false); + assertEquals(isRecordObjectOf(is.Number)({ [a]: "a" }), false); + assertEquals(isRecordObjectOf(is.String)({ [a]: true }), false); + }); + }); }); Deno.test("isRecordObjectOf", async (t) => { @@ -89,6 +104,7 @@ Deno.test("isRecordObjectOf", async (t) => { }); await t.step("checks only object's own properties", async (t) => { + const s = Symbol("s"); await t.step("returns true on T record", () => { assertEquals( isRecordObjectOf(is.Number, is.String)( @@ -115,6 +131,58 @@ Deno.test("isRecordObjectOf", async (t) => { true, "No own properties", ); + assertEquals( + isRecordObjectOf(is.String, is.String)( + Object.assign(Object.create({ [s]: "ignore" }), {/* empty */}), + ), + true, + "No own properties", + ); + }); + }); + + await t.step("with symbol properties", async (t) => { + const a = Symbol("a"); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRecordObjectOf(is.Number, is.Symbol).name); + await assertSnapshot( + t, + isRecordObjectOf((_x): _x is string => false, is.Symbol).name, + ); + }); + + await t.step("returns true on T record", () => { + assertEquals(isRecordObjectOf(is.Number, is.Symbol)({ [a]: 0 }), true); + assertEquals(isRecordObjectOf(is.String, is.Symbol)({ [a]: "a" }), true); + assertEquals( + isRecordObjectOf(is.Boolean, is.Symbol)({ [a]: true }), + true, + ); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordObjectOf(is.String, is.Symbol)({ [a]: 0 }), false); + assertEquals(isRecordObjectOf(is.Number, is.Symbol)({ [a]: "a" }), false); + assertEquals( + isRecordObjectOf(is.String, is.Symbol)({ [a]: true }), + false, + ); + }); + + await t.step("returns false on non K record", () => { + assertEquals(isRecordObjectOf(is.Number, is.String)({ [a]: 0 }), false); + assertEquals(isRecordObjectOf(is.String, is.String)({ [a]: "a" }), false); + assertEquals( + isRecordObjectOf(is.Boolean, is.String)({ [a]: true }), + false, + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0 }; + if (isRecordObjectOf(is.Number, is.Symbol)(a)) { + assertType>>(true); + } }); }); }); From 4cc6acf8e9bf3d972b463840b13121d39604d0fc Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 15:56:17 +0900 Subject: [PATCH 12/18] fix[isRecordOf]: checks only own properties --- is/record_of.ts | 3 ++- is/record_of_test.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/is/record_of.ts b/is/record_of.ts index 4676f71..60423ce 100644 --- a/is/record_of.ts +++ b/is/record_of.ts @@ -39,7 +39,8 @@ export function isRecordOf( return rewriteName( (x: unknown): x is Record => { if (!isRecord(x)) return false; - for (const k in x) { + const keys = Object.keys(x); + for (const k of keys) { if (!pred(x[k])) return false; if (predKey && !predKey(k)) return false; } diff --git a/is/record_of_test.ts b/is/record_of_test.ts index 91cf7d6..8093ec0 100644 --- a/is/record_of_test.ts +++ b/is/record_of_test.ts @@ -29,6 +29,29 @@ Deno.test("isRecordOf", async (t) => { assertType>>(true); } }); + + await t.step("checks only object's own properties", async (t) => { + await t.step("returns true on T record", () => { + assertEquals( + isRecordOf(is.Number)( + Object.assign(Object.create({ p: "ignore" }), { a: 0 }), + ), + true, + ); + assertEquals( + isRecordOf(is.String)( + Object.assign(Object.create({ p: 0 /* ignore */ }), { a: "a" }), + ), + true, + ); + assertEquals( + isRecordOf(is.Boolean)( + Object.assign(Object.create({ p: "ignore" }), { a: true }), + ), + true, + ); + }); + }); }); Deno.test("isRecordOf", async (t) => { @@ -64,4 +87,34 @@ Deno.test("isRecordOf", async (t) => { assertType>>(true); } }); + + await t.step("checks only object's own properties", async (t) => { + await t.step("returns true on T record", () => { + assertEquals( + isRecordOf(is.Number, is.String)( + Object.assign(Object.create({ p: "ignore" }), { a: 0 }), + ), + true, + ); + assertEquals( + isRecordOf(is.String, is.String)( + Object.assign(Object.create({ p: 0 /* ignore */ }), { a: "a" }), + ), + true, + ); + assertEquals( + isRecordOf(is.Boolean, is.String)( + Object.assign(Object.create({ p: "ignore" }), { a: true }), + ), + true, + ); + assertEquals( + isRecordOf(is.String, is.Number)( + Object.assign(Object.create({ p: "ignore" }), {/* empty */}), + ), + true, + "No own properties", + ); + }); + }); }); From 168367a094b707f441ba0e9764cde69b5a756830 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 9 Aug 2024 03:59:57 +0900 Subject: [PATCH 13/18] feat[isRecordOf]: checks own symbol key properties --- is/__snapshots__/record_of_test.ts.snap | 4 ++ is/record_of.ts | 5 ++- is/record_of_test.ts | 59 +++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/is/__snapshots__/record_of_test.ts.snap b/is/__snapshots__/record_of_test.ts.snap index aa1da92..0874713 100644 --- a/is/__snapshots__/record_of_test.ts.snap +++ b/is/__snapshots__/record_of_test.ts.snap @@ -7,3 +7,7 @@ snapshot[`isRecordOf > returns properly named predicate function 2`] = `"isRe snapshot[`isRecordOf > returns properly named predicate function 1`] = `"isRecordOf(isNumber, isString)"`; snapshot[`isRecordOf > returns properly named predicate function 2`] = `"isRecordOf((anonymous), isString)"`; + +snapshot[`isRecordOf > with symbol properties > returns properly named predicate function 1`] = `"isRecordOf(isNumber, isSymbol)"`; + +snapshot[`isRecordOf > with symbol properties > returns properly named predicate function 2`] = `"isRecordOf((anonymous), isSymbol)"`; diff --git a/is/record_of.ts b/is/record_of.ts index 60423ce..1d96cd2 100644 --- a/is/record_of.ts +++ b/is/record_of.ts @@ -39,7 +39,10 @@ export function isRecordOf( return rewriteName( (x: unknown): x is Record => { if (!isRecord(x)) return false; - const keys = Object.keys(x); + const keys = [ + ...Object.keys(x), + ...Object.getOwnPropertySymbols(x), + ]; for (const k of keys) { if (!pred(x[k])) return false; if (predKey && !predKey(k)) return false; diff --git a/is/record_of_test.ts b/is/record_of_test.ts index 8093ec0..a91fc33 100644 --- a/is/record_of_test.ts +++ b/is/record_of_test.ts @@ -52,6 +52,21 @@ Deno.test("isRecordOf", async (t) => { ); }); }); + + await t.step("with symbol properties", async (t) => { + const a = Symbol("a"); + await t.step("returns true on T record", () => { + assertEquals(isRecordOf(is.Number)({ [a]: 0 }), true); + assertEquals(isRecordOf(is.String)({ [a]: "a" }), true); + assertEquals(isRecordOf(is.Boolean)({ [a]: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordOf(is.String)({ [a]: 0 }), false); + assertEquals(isRecordOf(is.Number)({ [a]: "a" }), false); + assertEquals(isRecordOf(is.String)({ [a]: true }), false); + }); + }); }); Deno.test("isRecordOf", async (t) => { @@ -89,6 +104,7 @@ Deno.test("isRecordOf", async (t) => { }); await t.step("checks only object's own properties", async (t) => { + const s = Symbol("s"); await t.step("returns true on T record", () => { assertEquals( isRecordOf(is.Number, is.String)( @@ -115,6 +131,49 @@ Deno.test("isRecordOf", async (t) => { true, "No own properties", ); + assertEquals( + isRecordOf(is.String, is.String)( + Object.assign(Object.create({ [s]: "ignore" }), {/* empty */}), + ), + true, + "No own properties", + ); + }); + }); + + await t.step("with symbol properties", async (t) => { + const a = Symbol("a"); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRecordOf(is.Number, is.Symbol).name); + await assertSnapshot( + t, + isRecordOf((_x): _x is string => false, is.Symbol).name, + ); + }); + + await t.step("returns true on T record", () => { + assertEquals(isRecordOf(is.Number, is.Symbol)({ [a]: 0 }), true); + assertEquals(isRecordOf(is.String, is.Symbol)({ [a]: "a" }), true); + assertEquals(isRecordOf(is.Boolean, is.Symbol)({ [a]: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordOf(is.String, is.Symbol)({ [a]: 0 }), false); + assertEquals(isRecordOf(is.Number, is.Symbol)({ [a]: "a" }), false); + assertEquals(isRecordOf(is.String, is.Symbol)({ [a]: true }), false); + }); + + await t.step("returns false on non K record", () => { + assertEquals(isRecordOf(is.Number, is.String)({ [a]: 0 }), false); + assertEquals(isRecordOf(is.String, is.String)({ [a]: "a" }), false); + assertEquals(isRecordOf(is.Boolean, is.String)({ [a]: true }), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0 }; + if (isRecordOf(is.Number, is.Symbol)(a)) { + assertType>>(true); + } }); }); }); From 67dbc0888c08284b748ef034b7038ad80e4bb8ae Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 11 Aug 2024 08:04:19 +0900 Subject: [PATCH 14/18] refactor[isObjectOf]: improve performance --- is/object_of.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/is/object_of.ts b/is/object_of.ts index 91c0122..7121e64 100644 --- a/is/object_of.ts +++ b/is/object_of.ts @@ -39,6 +39,10 @@ import type { Predicate } from "../type.ts"; export function isObjectOf< T extends Record>, >(predObj: T): Predicate> & IsPredObj { + const preds: readonly [key: PropertyKey, pred: Predicate][] = [ + ...Object.keys(predObj), + ...Object.getOwnPropertySymbols(predObj), + ].map((k) => [k, predObj[k]]); return annotate( rewriteName( (x: unknown): x is ObjectOf => { @@ -47,11 +51,7 @@ export function isObjectOf< typeof x !== "object" && typeof x !== "function" || Array.isArray(x) ) return false; - // Check each values - return [ - ...Object.keys(predObj), - ...Object.getOwnPropertySymbols(predObj), - ].every((k) => predObj[k]((x as T)[k])); + return preds.every(([k, pred]) => pred((x as T)[k])); }, "isObjectOf", predObj, From 873097fac96c7941b07b68d010156a4808f3aef5 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 11 Aug 2024 08:48:42 +0900 Subject: [PATCH 15/18] test[isRequiredOf]: add proper `true` or `false` cases --- is/required_of_test.ts | 47 ++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/is/required_of_test.ts b/is/required_of_test.ts index 3f638b4..9cb4e40 100644 --- a/is/required_of_test.ts +++ b/is/required_of_test.ts @@ -19,25 +19,31 @@ Deno.test("isRequiredOf", async (t) => { }); await t.step("returns true on Required object", () => { + assertEquals(isRequiredOf(pred)({ a: 0, b: "a", c: true, d: "a" }), true); assertEquals( - isRequiredOf(pred)({ a: undefined, b: undefined, c: undefined }), - false, - "Object does not have required properties", - ); - assertEquals( - isRequiredOf(pred)({}), - false, - "Object does not have required properties", + isRequiredOf(pred)({ a: 0, b: undefined, c: true, d: "a" }), + true, + "Union type contains 'undefined'", ); }); await t.step("returns false on non Required object", () => { assertEquals(isRequiredOf(pred)("a"), false, "Value is not an object"); assertEquals( - isRequiredOf(pred)({ a: 0, b: "a", c: "" }), + isRequiredOf(pred)({ a: 0, b: "a", c: 0, d: "a" }), false, "Object have a different type property", ); + assertEquals( + isRequiredOf(pred)({ a: 0, b: "a", d: "a" }), + false, + "Object does not have required properties", + ); + assertEquals( + isRequiredOf(pred)({ a: 0, b: "a", c: undefined, d: "a" }), + false, + "Optional property that converted to required is 'undefined'", + ); }); await t.step("predicated type is correct", () => { @@ -69,24 +75,33 @@ Deno.test("isRequiredOf", async (t) => { await t.step("returns true on Required object", () => { assertEquals( - isRequiredOf(pred)({ a: undefined, [b]: undefined, [c]: undefined }), - false, - "Object does not have required properties", + isRequiredOf(pred)({ a: 0, [b]: "a", [c]: true, [d]: "a" }), + true, ); assertEquals( - isRequiredOf(pred)({}), - false, - "Object does not have required properties", + isRequiredOf(pred)({ a: 0, [b]: undefined, [c]: true, [d]: "a" }), + true, + "Union type contains 'undefined'", ); }); await t.step("returns false on non Required object", () => { assertEquals(isRequiredOf(pred)("a"), false, "Value is not an object"); assertEquals( - isRequiredOf(pred)({ a: 0, [b]: "a", [c]: "" }), + isRequiredOf(pred)({ a: 0, [b]: "a", [c]: 0, [d]: "a" }), false, "Object have a different type property", ); + assertEquals( + isRequiredOf(pred)({ a: 0, [b]: "a", [d]: "a" }), + false, + "Object does not have required properties", + ); + assertEquals( + isRequiredOf(pred)({ a: 0, [b]: "a", [c]: undefined, [d]: "a" }), + false, + "Optional property that converted to required is 'undefined'", + ); }); await t.step("predicated type is correct", () => { From e7185c4b1b54b40dbccc5dfc5ce0a7c977d0d8b1 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 11 Aug 2024 08:24:28 +0900 Subject: [PATCH 16/18] fix[isTupleOf]: rename `predElse` to `predRest` --- is/__snapshots__/tuple_of_test.ts.snap | 6 +++--- is/tuple_of.ts | 24 ++++++++++++------------ is/tuple_of_test.ts | 20 ++++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/is/__snapshots__/tuple_of_test.ts.snap b/is/__snapshots__/tuple_of_test.ts.snap index 2098ae2..ff7bd39 100644 --- a/is/__snapshots__/tuple_of_test.ts.snap +++ b/is/__snapshots__/tuple_of_test.ts.snap @@ -22,7 +22,7 @@ snapshot[`isTupleOf > returns properly named predicate function 3`] = ` ])" `; -snapshot[`isTupleOf > returns properly named predicate function 1`] = ` +snapshot[`isTupleOf > returns properly named predicate function 1`] = ` "isTupleOf([ isNumber, isString, @@ -30,9 +30,9 @@ snapshot[`isTupleOf > returns properly named predicate function 1`] = ` ], isArray)" `; -snapshot[`isTupleOf > returns properly named predicate function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isTupleOf > returns properly named predicate function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isTupleOf > returns properly named predicate function 3`] = ` +snapshot[`isTupleOf > returns properly named predicate function 3`] = ` "isTupleOf([ isTupleOf([ isTupleOf([ diff --git a/is/tuple_of.ts b/is/tuple_of.ts index ac0d2f6..bed8c17 100644 --- a/is/tuple_of.ts +++ b/is/tuple_of.ts @@ -3,7 +3,7 @@ import type { Predicate, PredicateType } from "../type.ts"; import { isArray } from "./array.ts"; /** - * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. + * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. * * Use {@linkcode isUniformTupleOf} to check if the type of `x` is a tuple of uniform types. * @@ -19,7 +19,7 @@ import { isArray } from "./array.ts"; * } * ``` * - * With `predElse`: + * With `predRest` to represent rest elements: * * ```ts * import { is } from "@core/unknownutil"; @@ -56,20 +56,20 @@ export function isTupleOf< export function isTupleOf< T extends readonly [Predicate, ...Predicate[]], - E extends Predicate, + R extends Predicate, >( predTup: T, - predElse: E, -): Predicate<[...TupleOf, ...PredicateType]>; + predRest: R, +): Predicate<[...TupleOf, ...PredicateType]>; export function isTupleOf< T extends readonly [Predicate, ...Predicate[]], - E extends Predicate, + R extends Predicate, >( predTup: T, - predElse?: E, -): Predicate | [...TupleOf, ...PredicateType]> { - if (!predElse) { + predRest?: R, +): Predicate | [...TupleOf, ...PredicateType]> { + if (!predRest) { return rewriteName( (x: unknown): x is TupleOf => { if (!isArray(x) || x.length !== predTup.length) { @@ -82,17 +82,17 @@ export function isTupleOf< ); } else { return rewriteName( - (x: unknown): x is [...TupleOf, ...PredicateType] => { + (x: unknown): x is [...TupleOf, ...PredicateType] => { if (!isArray(x) || x.length < predTup.length) { return false; } const head = x.slice(0, predTup.length); const tail = x.slice(predTup.length); - return predTup.every((pred, i) => pred(head[i])) && predElse(tail); + return predTup.every((pred, i) => pred(head[i])) && predRest(tail); }, "isTupleOf", predTup, - predElse, + predRest, ); } } diff --git a/is/tuple_of_test.ts b/is/tuple_of_test.ts index c930390..e273efc 100644 --- a/is/tuple_of_test.ts +++ b/is/tuple_of_test.ts @@ -43,7 +43,7 @@ Deno.test("isTupleOf", async (t) => { }); }); -Deno.test("isTupleOf", async (t) => { +Deno.test("isTupleOf", async (t) => { await t.step("returns properly named predicate function", async (t) => { await assertSnapshot( t, @@ -67,27 +67,27 @@ Deno.test("isTupleOf", async (t) => { await t.step("returns true on T tuple", () => { const predTup = [is.Number, is.String, is.Boolean] as const; - const predElse = is.ArrayOf(is.Number); - assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), true); + const predRest = is.ArrayOf(is.Number); + assertEquals(isTupleOf(predTup, predRest)([0, "a", true, 0, 1, 2]), true); }); await t.step("returns false on non T tuple", () => { const predTup = [is.Number, is.String, is.Boolean] as const; - const predElse = is.ArrayOf(is.String); - assertEquals(isTupleOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); - assertEquals(isTupleOf(predTup, predElse)([0, "a", 0, 1, 2]), false); + const predRest = is.ArrayOf(is.String); + assertEquals(isTupleOf(predTup, predRest)([0, 1, 2, 0, 1, 2]), false); + assertEquals(isTupleOf(predTup, predRest)([0, "a", 0, 1, 2]), false); assertEquals( - isTupleOf(predTup, predElse)([0, "a", true, 0, 0, 1, 2]), + isTupleOf(predTup, predRest)([0, "a", true, 0, 0, 1, 2]), false, ); - assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), false); + assertEquals(isTupleOf(predTup, predRest)([0, "a", true, 0, 1, 2]), false); }); await t.step("predicated type is correct", () => { const predTup = [is.Number, is.String, is.Boolean] as const; - const predElse = is.ArrayOf(is.Number); + const predRest = is.ArrayOf(is.Number); const a: unknown = [0, "a", true, 0, 1, 2]; - if (isTupleOf(predTup, predElse)(a)) { + if (isTupleOf(predTup, predRest)(a)) { assertType>( true, ); From 0c106ce4b8e15af1da5a900cc5a2098a8fa84934 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 11 Aug 2024 08:25:33 +0900 Subject: [PATCH 17/18] fix[isParametersOf]: rename `predElse` to `predRest` --- is/__snapshots__/parameters_of_test.ts.snap | 8 +++---- is/parameters_of.ts | 12 +++++----- is/parameters_of_test.ts | 26 ++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/is/__snapshots__/parameters_of_test.ts.snap b/is/__snapshots__/parameters_of_test.ts.snap index d1de1ce..f37c835 100644 --- a/is/__snapshots__/parameters_of_test.ts.snap +++ b/is/__snapshots__/parameters_of_test.ts.snap @@ -24,7 +24,7 @@ snapshot[`isParametersOf > returns properly named predicate function 4`] = ` ])" `; -snapshot[`isParametersOf > returns properly named predicate function 1`] = ` +snapshot[`isParametersOf > returns properly named predicate function 1`] = ` "isParametersOf([ isNumber, isString, @@ -32,11 +32,11 @@ snapshot[`isParametersOf > returns properly named predicate function 1`] = ], isArray)" `; -snapshot[`isParametersOf > returns properly named predicate function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isParametersOf > returns properly named predicate function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isParametersOf > returns properly named predicate function 3`] = `"isParametersOf([], isArrayOf(isString))"`; +snapshot[`isParametersOf > returns properly named predicate function 3`] = `"isParametersOf([], isArrayOf(isString))"`; -snapshot[`isParametersOf > returns properly named predicate function 4`] = ` +snapshot[`isParametersOf > returns properly named predicate function 4`] = ` "isParametersOf([ isParametersOf([ isParametersOf([ diff --git a/is/parameters_of.ts b/is/parameters_of.ts index 819d95c..a202ca7 100644 --- a/is/parameters_of.ts +++ b/is/parameters_of.ts @@ -28,7 +28,7 @@ import { isArray } from "./array.ts"; * } * ``` * - * With `predElse`: + * With `predRest` to represent rest parameters: * * ```ts * import { as, is } from "@core/unknownutil"; @@ -71,18 +71,18 @@ export function isParametersOf< E extends Predicate, >( predTup: T, - predElse: E, + predRest: E, ): Predicate<[...ParametersOf, ...PredicateType]>; export function isParametersOf< T extends readonly [...Predicate[]], E extends Predicate, >( predTup: T, - predElse?: E, + predRest?: E, ): Predicate | [...ParametersOf, ...PredicateType]> { const requiresLength = 1 + predTup.findLastIndex((pred) => !hasOptional(pred)); - if (!predElse) { + if (!predRest) { return rewriteName( (x: unknown): x is ParametersOf => { if ( @@ -103,11 +103,11 @@ export function isParametersOf< } const head = x.slice(0, predTup.length); const tail = x.slice(predTup.length); - return predTup.every((pred, i) => pred(head[i])) && predElse(tail); + return predTup.every((pred, i) => pred(head[i])) && predRest(tail); }, "isParametersOf", predTup, - predElse, + predRest, ); } } diff --git a/is/parameters_of_test.ts b/is/parameters_of_test.ts index 6bae4b2..478d806 100644 --- a/is/parameters_of_test.ts +++ b/is/parameters_of_test.ts @@ -59,7 +59,7 @@ Deno.test("isParametersOf", async (t) => { }); }); -Deno.test("isParametersOf", async (t) => { +Deno.test("isParametersOf", async (t) => { await t.step("returns properly named predicate function", async (t) => { assertEquals(typeof isParametersOf([], is.Array), "function"); await assertSnapshot( @@ -92,32 +92,32 @@ Deno.test("isParametersOf", async (t) => { await t.step("returns true on T tuple", () => { const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; - const predElse = is.ArrayOf(is.Number); + const predRest = is.ArrayOf(is.Number); assertEquals( - isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), + isParametersOf(predTup, predRest)([0, "a", true, 0, 1, 2]), true, ); assertEquals( - isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), + isParametersOf(predTup, predRest)([0, "a", undefined, 0, 1, 2]), true, ); - assertEquals(isParametersOf(predTup, predElse)([0, "a"]), true); + assertEquals(isParametersOf(predTup, predRest)([0, "a"]), true); }); await t.step("returns false on non T tuple", () => { const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; - const predElse = is.ArrayOf(is.String); - assertEquals(isParametersOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); - assertEquals(isParametersOf(predTup, predElse)([0, "a", 0, 1, 2]), false); + const predRest = is.ArrayOf(is.String); + assertEquals(isParametersOf(predTup, predRest)([0, 1, 2, 0, 1, 2]), false); + assertEquals(isParametersOf(predTup, predRest)([0, "a", 0, 1, 2]), false); assertEquals( - isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), + isParametersOf(predTup, predRest)([0, "a", true, 0, 1, 2]), false, ); assertEquals( - isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), + isParametersOf(predTup, predRest)([0, "a", undefined, 0, 1, 2]), false, ); - assertEquals(isParametersOf(predTup, predElse)([0, "a", "b"]), false); + assertEquals(isParametersOf(predTup, predRest)([0, "a", "b"]), false); }); await t.step("predicated type is correct", () => { @@ -127,9 +127,9 @@ Deno.test("isParametersOf", async (t) => { as.Optional(is.String), as.Optional(is.Boolean), ] as const; - const predElse = is.ArrayOf(is.Number); + const predRest = is.ArrayOf(is.Number); const a: unknown = [0, "a"]; - if (isParametersOf(predTup, predElse)(a)) { + if (isParametersOf(predTup, predRest)(a)) { assertType< Equal< typeof a, From 4a5dc04a74a453efa62d8c45d3223ade13863bff Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 11 Aug 2024 09:42:02 +0900 Subject: [PATCH 18/18] docs: apply `deno task gen` --- is/mod.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/is/mod.ts b/is/mod.ts index e6a920e..8952e39 100644 --- a/is/mod.ts +++ b/is/mod.ts @@ -455,7 +455,7 @@ export const is: { * } * ``` * - * With `predElse`: + * With `predRest` to represent rest parameters: * * ```ts * import { as, is } from "@core/unknownutil"; @@ -823,7 +823,7 @@ export const is: { */ SyncFunction: typeof isSyncFunction; /** - * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. + * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. * * Use {@linkcode isUniformTupleOf} to check if the type of `x` is a tuple of uniform types. * @@ -839,7 +839,7 @@ export const is: { * } * ``` * - * With `predElse`: + * With `predRest` to represent rest elements: * * ```ts * import { is } from "@core/unknownutil";