diff --git a/is.ts b/is.ts index 4b72d00..1ee9fb0 100644 --- a/is.ts +++ b/is.ts @@ -138,8 +138,13 @@ export function isRecord( export function isRecordOf( pred: Predicate, ): Predicate> { - return (x: unknown): x is RecordOf => - isRecord(x) && Object.values(x).every(pred); + return (x: unknown): x is RecordOf => { + if (!isRecord(x)) return false; + for (const k in x) { + if (!pred(x[k])) return false; + } + return true; + }; } type FlatType = T extends RecordOf @@ -188,16 +193,35 @@ export function isObjectOf< predObj: T, options: { strict?: boolean } = {}, ): Predicate> { - const preds = Object.entries(predObj); - const allKeys = new Set(preds.map(([key]) => key)); - const requiredKeys = preds - .filter(([_, pred]) => !(pred as OptionalPredicate).optional) - .map(([key]) => key); - const hasKeys = options.strict - ? (props: string[]) => props.every((p) => allKeys.has(p)) - : (props: string[]) => requiredKeys.every((k) => props.includes(k)); - return (x: unknown): x is ObjectOf => - isRecord(x) && hasKeys(Object.keys(x)) && preds.every(([k, p]) => p(x[k])); + return options.strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj); +} + +function isObjectOfLoose< + T extends RecordOf>, +>( + predObj: T, +): Predicate> { + return (x: unknown): x is ObjectOf => { + if (!isRecord(x)) return false; + for (const k in predObj) { + if (!predObj[k](x[k])) return false; + } + return true; + }; +} + +function isObjectOfStrict< + T extends RecordOf>, +>( + predObj: T, +): Predicate> { + const keys = new Set(Object.keys(predObj)); + const pred = isObjectOfLoose(predObj); + return (x: unknown): x is ObjectOf => { + if (!pred(x)) return false; + const ks = Object.keys(x); + return ks.length <= keys.size && ks.every((k) => keys.has(k)); + }; } /** @@ -366,9 +390,12 @@ export type OptionalPredicate = Predicate & { export function isOptionalOf( pred: Predicate, ): OptionalPredicate { - return Object.assign(isOneOf([isUndefined, pred]), { - optional: true as const, - }); + return Object.assign( + (x: unknown): x is Predicate => isUndefined(x) || pred(x), + { + optional: true as const, + }, + ) as OptionalPredicate; } export default { diff --git a/is_bench.ts b/is_bench.ts index 2867d6c..06aba3f 100644 --- a/is_bench.ts +++ b/is_bench.ts @@ -170,6 +170,15 @@ Deno.bench({ } }, }); +Deno.bench({ + name: "is.ObjectOf (strict)", + fn: () => { + const pred = is.ObjectOf(predObj, { strict: true }); + for (const c of cs) { + pred(c); + } + }, +}); const isObjectOfPred = is.ObjectOf(predObj); Deno.bench({ @@ -181,6 +190,16 @@ Deno.bench({ }, }); +const isObjectOfStrictPred = is.ObjectOf(predObj, { strict: true }); +Deno.bench({ + name: "is.ObjectOf (pre, strict)", + fn: () => { + for (const c of cs) { + isObjectOfStrictPred(c); + } + }, +}); + Deno.bench({ name: "is.Function", fn: () => { diff --git a/is_test.ts b/is_test.ts index 0649545..e5ecfc4 100644 --- a/is_test.ts +++ b/is_test.ts @@ -280,7 +280,7 @@ Deno.test("isObjectOf", async (t) => { }); await testWithExamples( t, - isObjectOf({ a: (_: unknown): _ is unknown => true }), + isObjectOf({ a: (_: unknown): _ is unknown => false }), { excludeExamples: ["record"] }, ); await t.step("with optional properties", async (t) => {