Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,13 @@ export function isRecord(
export function isRecordOf<T>(
pred: Predicate<T>,
): Predicate<RecordOf<T>> {
return (x: unknown): x is RecordOf<T> =>
isRecord(x) && Object.values(x).every(pred);
return (x: unknown): x is RecordOf<T> => {
if (!isRecord(x)) return false;
for (const k in x) {
if (!pred(x[k])) return false;
}
return true;
};
}

type FlatType<T> = T extends RecordOf<unknown>
Expand Down Expand Up @@ -188,16 +193,35 @@ export function isObjectOf<
predObj: T,
options: { strict?: boolean } = {},
): Predicate<ObjectOf<T>> {
const preds = Object.entries(predObj);
const allKeys = new Set(preds.map(([key]) => key));
const requiredKeys = preds
.filter(([_, pred]) => !(pred as OptionalPredicate<unknown>).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<T> =>
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<Predicate<unknown>>,
>(
predObj: T,
): Predicate<ObjectOf<T>> {
return (x: unknown): x is ObjectOf<T> => {
if (!isRecord(x)) return false;
for (const k in predObj) {
if (!predObj[k](x[k])) return false;
}
return true;
};
}

function isObjectOfStrict<
T extends RecordOf<Predicate<unknown>>,
>(
predObj: T,
): Predicate<ObjectOf<T>> {
const keys = new Set(Object.keys(predObj));
const pred = isObjectOfLoose(predObj);
return (x: unknown): x is ObjectOf<T> => {
if (!pred(x)) return false;
const ks = Object.keys(x);
return ks.length <= keys.size && ks.every((k) => keys.has(k));
};
}

/**
Expand Down Expand Up @@ -366,9 +390,12 @@ export type OptionalPredicate<T> = Predicate<T | undefined> & {
export function isOptionalOf<T>(
pred: Predicate<T>,
): OptionalPredicate<T> {
return Object.assign(isOneOf([isUndefined, pred]), {
optional: true as const,
});
return Object.assign(
(x: unknown): x is Predicate<T | undefined> => isUndefined(x) || pred(x),
{
optional: true as const,
},
) as OptionalPredicate<T>;
}

export default {
Expand Down
19 changes: 19 additions & 0 deletions is_bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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: () => {
Expand Down
2 changes: 1 addition & 1 deletion is_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ Deno.test("isObjectOf<T>", 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) => {
Expand Down