Skip to content

Commit 1ab032a

Browse files
committed
👍 Add isReadonlyOf
1 parent f0392ca commit 1ab032a

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const snapshot = {};
2+
3+
snapshot[`isReadonlyOf<T> > returns properly named function 1`] = `
4+
"isObjectOf({
5+
a: asReadonly(isNumber),
6+
b: asReadonly(isUnionOf([
7+
isString,
8+
isUndefined
9+
])),
10+
c: asReadonly(isBoolean)
11+
})"
12+
`;
13+
14+
snapshot[`isReadonlyOf<T> > returns properly named function 2`] = `
15+
"isObjectOf({
16+
a: asReadonly(isNumber),
17+
b: asReadonly(isUnionOf([
18+
isString,
19+
isUndefined
20+
])),
21+
c: asReadonly(isBoolean)
22+
})"
23+
`;

is/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { isParametersOf } from "./parameters_of.ts";
2020
import { isPartialOf } from "./partial_of.ts";
2121
import { isPickOf } from "./pick_of.ts";
2222
import { isPrimitive } from "./primitive.ts";
23+
import { isReadonlyOf } from "./readonly_of.ts";
2324
import { isRecord } from "./record.ts";
2425
import { isRecordObject } from "./record_object.ts";
2526
import { isRecordObjectOf } from "./record_object_of.ts";
@@ -60,6 +61,7 @@ export const is = {
6061
PartialOf: isPartialOf,
6162
PickOf: isPickOf,
6263
Primitive: isPrimitive,
64+
ReadonlyOf: isReadonlyOf,
6365
Record: isRecord,
6466
RecordObject: isRecordObject,
6567
RecordObjectOf: isRecordObjectOf,

is/readonly_of.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { FlatType } from "../_typeutil.ts";
2+
import type { WithPredObj } from "../_annotation.ts";
3+
import { asReadonly } from "../as/readonly.ts";
4+
import type { Predicate } from "../type.ts";
5+
import { isObjectOf } from "../is/object_of.ts";
6+
7+
/**
8+
* Return a type predicate function that returns `true` if the type of `x` is `Readonly<ObjectOf<T>>`.
9+
*
10+
* To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost.
11+
*
12+
* ```typescript
13+
* import { as, is } from "@core/unknownutil";
14+
*
15+
* const isMyType = is.ReadonlyOf(is.ObjectOf({
16+
* a: is.Number,
17+
* b: is.UnionOf([is.String, is.Undefined]),
18+
* c: as.Readonly(is.Boolean),
19+
* }));
20+
* const a: unknown = { a: 0, b: "b", c: true };
21+
* if (isMyType(a)) {
22+
* // 'a' is narrowed to { readonly a: number; readonly b: string | undefined; readonly c: boolean }
23+
* const _: { readonly a: number; readonly b: string | undefined; readonly c: boolean } = a;
24+
* }
25+
* ```
26+
*/
27+
export function isReadonlyOf<
28+
T extends Record<PropertyKey, unknown>,
29+
P extends Record<PropertyKey, Predicate<unknown>>,
30+
>(
31+
pred: Predicate<T> & WithPredObj<P>,
32+
):
33+
& Predicate<FlatType<Readonly<T>>>
34+
& WithPredObj<P> {
35+
const predObj = Object.fromEntries(
36+
Object.entries(pred.predObj).map(([k, v]) => [k, asReadonly(v)]),
37+
) as Record<PropertyKey, Predicate<unknown>>;
38+
return isObjectOf(predObj) as
39+
& Predicate<FlatType<Readonly<T>>>
40+
& WithPredObj<P>;
41+
}

is/readonly_of_test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { assertEquals } from "@std/assert";
2+
import { assertSnapshot } from "@std/testing/snapshot";
3+
import { assertType } from "@std/testing/types";
4+
import type { Equal } from "../_testutil.ts";
5+
import { as } from "../as/mod.ts";
6+
import { is } from "../is/mod.ts";
7+
import { isReadonlyOf } from "./readonly_of.ts";
8+
9+
Deno.test("isReadonlyOf<T>", async (t) => {
10+
const pred = is.ObjectOf({
11+
a: is.Number,
12+
b: is.UnionOf([is.String, is.Undefined]),
13+
c: as.Readonly(is.Boolean),
14+
});
15+
await t.step("returns properly named function", async (t) => {
16+
await assertSnapshot(t, isReadonlyOf(pred).name);
17+
// Nestable (no effect)
18+
await assertSnapshot(t, isReadonlyOf(isReadonlyOf(pred)).name);
19+
});
20+
await t.step("returns proper type predicate", () => {
21+
const a: unknown = { a: 0, b: "a", c: true };
22+
if (isReadonlyOf(pred)(a)) {
23+
assertType<
24+
Equal<
25+
typeof a,
26+
Readonly<{ a: number; b: string | undefined; c: boolean }>
27+
>
28+
>(true);
29+
}
30+
});
31+
await t.step("returns true on Readonly<T> object", () => {
32+
assertEquals(
33+
isReadonlyOf(pred)({ a: 0, b: "b", c: true } as const),
34+
true,
35+
);
36+
assertEquals(
37+
isReadonlyOf(pred)({ a: 0, b: "b", c: true }),
38+
true,
39+
);
40+
});
41+
await t.step("returns false on non Readonly<T> object", () => {
42+
assertEquals(isReadonlyOf(pred)("a"), false, "Value is not an object");
43+
assertEquals(
44+
isReadonlyOf(pred)({ a: 0, b: "a", c: "" }),
45+
false,
46+
"Object have a different type property",
47+
);
48+
});
49+
});

0 commit comments

Comments
 (0)