Skip to content

Commit f0392ca

Browse files
committed
👍 Add as.Readonly and as.Unreadonly
1 parent 67ef8f9 commit f0392ca

File tree

6 files changed

+170
-6
lines changed

6 files changed

+170
-6
lines changed

_annotation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ export type WithOptional<T = unknown> = {
3636
optional: Predicate<T>;
3737
};
3838

39+
/**
40+
* Annotation for readonly.
41+
*/
42+
export type WithReadonly<T = unknown> = {
43+
readonly: Predicate<T>;
44+
};
45+
3946
/**
4047
* Annotation for predObj.
4148
*/
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const snapshot = {};
2+
3+
snapshot[`asReadonly<T> > returns properly named function 1`] = `"asReadonly(isNumber)"`;
4+
5+
snapshot[`asReadonly<T> > returns properly named function 2`] = `"asReadonly(isNumber)"`;
6+
7+
snapshot[`asUnreadonly<T> > returns properly named function 1`] = `"isNumber"`;
8+
9+
snapshot[`asUnreadonly<T> > returns properly named function 2`] = `"isNumber"`;
10+
11+
snapshot[`asUnreadonly<T> > returns properly named function 3`] = `"isNumber"`;

as/mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { asOptional, asUnoptional } from "./optional.ts";
2+
import { asReadonly, asUnreadonly } from "./readonly.ts";
23

34
export const as = {
45
Optional: asOptional,
6+
Readonly: asReadonly,
57
Unoptional: asUnoptional,
8+
Unreadonly: asUnreadonly,
69
};

as/readonly.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { rewriteName } from "../_funcutil.ts";
2+
import type { Predicate } from "../type.ts";
3+
import {
4+
annotate,
5+
hasAnnotation,
6+
unannotate,
7+
type WithReadonly,
8+
} from "../_annotation.ts";
9+
10+
/**
11+
* Return an `Readonly` annotated type predicate function that returns `true` if the type of `x` is `T`.
12+
*
13+
* To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost.
14+
*
15+
* ```ts
16+
* import { as, is } from "@core/unknownutil";
17+
*
18+
* const isMyType = is.ObjectOf({
19+
* foo: as.Readonly(is.String),
20+
* });
21+
* const a: unknown = {};
22+
* if (isMyType(a)) {
23+
* // a is narrowed to {readonly foo: string}
24+
* const _: {readonly foo: string} = a;
25+
* }
26+
* ```
27+
*/
28+
export function asReadonly<T>(
29+
pred: Predicate<T>,
30+
): Predicate<T> & WithReadonly {
31+
if (hasAnnotation(pred, "readonly")) {
32+
return pred as Predicate<T> & WithReadonly;
33+
}
34+
return rewriteName(
35+
annotate(
36+
(x: unknown): x is T => pred(x),
37+
"readonly",
38+
pred,
39+
),
40+
"asReadonly",
41+
pred,
42+
) as Predicate<T> & WithReadonly;
43+
}
44+
45+
/**
46+
* Return an `Readonly` un-annotated type predicate function that returns `true` if the type of `x` is `T`.
47+
*
48+
* To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost.
49+
*
50+
* ```ts
51+
* import { as, is } from "@core/unknownutil";
52+
*
53+
* const isMyType = is.ObjectOf({
54+
* foo: as.Unreadonly(as.Readonly(is.String)),
55+
* });
56+
* const a: unknown = {foo: "a"};
57+
* if (isMyType(a)) {
58+
* // a is narrowed to {foo: string}
59+
* const _: {foo: string} = a;
60+
* }
61+
* ```
62+
*/
63+
export function asUnreadonly<
64+
P extends Predicate<unknown>,
65+
T extends P extends Predicate<infer T> ? T : never,
66+
>(pred: P): Predicate<T> {
67+
if (!hasAnnotation(pred, "readonly")) {
68+
return pred as Predicate<T>;
69+
}
70+
return unannotate(pred, "readonly") as Predicate<T>;
71+
}
72+
73+
/**
74+
* Check if the given type predicate has readonly annotation.
75+
*/
76+
export function hasReadonly<
77+
P extends Predicate<unknown>,
78+
>(
79+
pred: P,
80+
): pred is P & WithReadonly {
81+
return hasAnnotation(pred, "readonly");
82+
}

as/readonly_test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { assertSnapshot } from "@std/testing/snapshot";
2+
import { assertType } from "@std/testing/types";
3+
import type { Equal } from "../_testutil.ts";
4+
import { is } from "../is/mod.ts";
5+
import { asReadonly, asUnreadonly } from "./readonly.ts";
6+
7+
Deno.test("asReadonly<T>", async (t) => {
8+
await t.step("returns properly named function", async (t) => {
9+
await assertSnapshot(t, asReadonly(is.Number).name);
10+
// Nesting does nothing
11+
await assertSnapshot(t, asReadonly(asReadonly(is.Number)).name);
12+
});
13+
await t.step("returns proper type predicate", () => {
14+
const a: unknown = undefined;
15+
if (is.ObjectOf({ a: asReadonly(is.Number) })(a)) {
16+
assertType<Equal<typeof a, { readonly a: number }>>(true);
17+
}
18+
});
19+
});
20+
21+
Deno.test("asUnreadonly<T>", async (t) => {
22+
await t.step("returns properly named function", async (t) => {
23+
await assertSnapshot(t, asUnreadonly(asReadonly(is.Number)).name);
24+
// Non optional does nothing
25+
await assertSnapshot(t, asUnreadonly(is.Number).name);
26+
// Nesting does nothing
27+
await assertSnapshot(
28+
t,
29+
asUnreadonly(asUnreadonly(asReadonly(is.Number))).name,
30+
);
31+
});
32+
await t.step("returns proper type predicate", () => {
33+
const a: unknown = undefined;
34+
if (is.ObjectOf({ a: asUnreadonly(asReadonly(is.Number)) })(a)) {
35+
assertType<Equal<typeof a, { a: number }>>(true);
36+
}
37+
});
38+
});

is/object_of.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
annotate,
55
type WithOptional,
66
type WithPredObj,
7+
type WithReadonly,
78
} from "../_annotation.ts";
89
import type { Predicate } from "../type.ts";
910

@@ -58,14 +59,36 @@ export function isObjectOf<
5859
}
5960

6061
type ObjectOf<T extends Record<PropertyKey, Predicate<unknown>>> = FlatType<
61-
// Optional
62+
// Readonly/Optional
6263
& {
63-
[K in keyof T as T[K] extends WithOptional<unknown> ? K : never]?:
64-
T[K] extends Predicate<infer U> ? U : never;
64+
readonly [
65+
K in keyof T as T[K] extends WithReadonly
66+
? T[K] extends WithOptional ? K : never
67+
: never
68+
]?: T[K] extends Predicate<infer U> ? U : never;
6569
}
66-
// Non optional
70+
// Readonly/Non optional
6771
& {
68-
[K in keyof T as T[K] extends WithOptional<unknown> ? never : K]:
69-
T[K] extends Predicate<infer U> ? U : never;
72+
readonly [
73+
K in keyof T as T[K] extends WithReadonly
74+
? T[K] extends WithOptional ? never : K
75+
: never
76+
]: T[K] extends Predicate<infer U> ? U : never;
77+
}
78+
// Non readonly/Optional
79+
& {
80+
[
81+
K in keyof T as T[K] extends WithReadonly ? never
82+
: T[K] extends WithOptional ? K
83+
: never
84+
]?: T[K] extends Predicate<infer U> ? U : never;
85+
}
86+
// Non readonly/Non optional
87+
& {
88+
[
89+
K in keyof T as T[K] extends WithReadonly ? never
90+
: T[K] extends WithOptional ? never
91+
: K
92+
]: T[K] extends Predicate<infer U> ? U : never;
7093
}
7194
>;

0 commit comments

Comments
 (0)