ts-fortress 2.2.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  export * from './intersection.mjs';
2
2
  export * from './merge-records.mjs';
3
+ export * from './recursion.mjs';
3
4
  export * from './union.mjs';
4
5
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/compose/index.mts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/compose/index.mts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { intersection } from './intersection.mjs';
2
2
  export { mergeRecords } from './merge-records.mjs';
3
+ export { recursion } from './recursion.mjs';
3
4
  export { union } from './union.mjs';
4
5
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -0,0 +1,5 @@
1
+ import { type Type } from '../type.mjs';
2
+ export declare const recursion: <A>(typeName: string, definition: () => Type<A>, options?: Partial<Readonly<{
3
+ defaultValue: A;
4
+ }>>) => Type<A>;
5
+ //# sourceMappingURL=recursion.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recursion.d.mts","sourceRoot":"","sources":["../../src/compose/recursion.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAGxC,eAAO,MAAM,SAAS,GAAI,CAAC,EACzB,UAAU,MAAM,EAChB,YAAY,MAAM,IAAI,CAAC,CAAC,CAAC,EACzB,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CACH,KACA,IAAI,CAAC,CAAC,CAqCR,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { createAssertFn } from '../utils/create-assert-fn.mjs';
2
+ import { createCastFn } from '../utils/create-cast-fn.mjs';
3
+ import { createIsFn } from '../utils/create-is-fn.mjs';
4
+ import 'ts-data-forge';
5
+
6
+ const recursion = (typeName, definition, options) => {
7
+ const cache = {
8
+ innerType: undefined,
9
+ computedDefaultValue: undefined,
10
+ };
11
+ const getInnerType = () => {
12
+ // eslint-disable-next-line functional/immutable-data
13
+ cache.innerType ??= definition();
14
+ return cache.innerType;
15
+ };
16
+ const validate = (a) => getInnerType().validate(a);
17
+ const is = createIsFn(validate);
18
+ const fill = (a) => getInnerType().fill(a);
19
+ return {
20
+ typeName,
21
+ get defaultValue() {
22
+ if (options?.defaultValue !== undefined) {
23
+ return options.defaultValue;
24
+ }
25
+ // eslint-disable-next-line functional/immutable-data
26
+ cache.computedDefaultValue ??= getInnerType().defaultValue;
27
+ return cache.computedDefaultValue;
28
+ },
29
+ fill,
30
+ validate,
31
+ is,
32
+ assertIs: createAssertFn(validate),
33
+ cast: createCastFn(validate),
34
+ };
35
+ };
36
+
37
+ export { recursion };
38
+ //# sourceMappingURL=recursion.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recursion.mjs","sources":["../../src/compose/recursion.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;AAGO,MAAM,SAAS,GAAG,CACvB,QAAgB,EAChB,UAAyB,EACzB,OAIC,KACU;AACX,IAAA,MAAM,KAAK,GAGP;AACF,QAAA,SAAS,EAAE,SAAgC;AAC3C,QAAA,oBAAoB,EAAE,SAA0B;KACjD;IAED,MAAM,YAAY,GAAG,MAAc;;AAEjC,QAAA,KAAK,CAAC,SAAS,KAAK,UAAU,EAAE;QAChC,OAAO,KAAK,CAAC,SAAS;AACxB,IAAA,CAAC;AAED,IAAA,MAAM,QAAQ,GAAwB,CAAC,CAAC,KAAK,YAAY,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEvE,IAAA,MAAM,EAAE,GAAG,UAAU,CAAI,QAAQ,CAAC;AAElC,IAAA,MAAM,IAAI,GAAoB,CAAC,CAAC,KAAK,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAE3D,OAAO;QACL,QAAQ;AACR,QAAA,IAAI,YAAY,GAAA;AACd,YAAA,IAAI,OAAO,EAAE,YAAY,KAAK,SAAS,EAAE;gBACvC,OAAO,OAAO,CAAC,YAAY;YAC7B;;AAEA,YAAA,KAAK,CAAC,oBAAoB,KAAK,YAAY,EAAE,CAAC,YAAY;YAC1D,OAAO,KAAK,CAAC,oBAAoB;QACnC,CAAC;QACD,IAAI;QACJ,QAAQ;QACR,EAAE;AACF,QAAA,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC;AAClC,QAAA,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC;KAC7B;AACH;;;;"}
package/dist/index.d.mts CHANGED
@@ -2,6 +2,7 @@ export * from './array/index.mjs';
2
2
  export * from './branded/index.mjs';
3
3
  export * from './compose/index.mjs';
4
4
  export * from './enum/index.mjs';
5
+ export * from './predefined/index.mjs';
5
6
  export * from './primitives/index.mjs';
6
7
  export * from './record/index.mjs';
7
8
  export * from './type.mjs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
package/dist/index.mjs CHANGED
@@ -23,9 +23,11 @@ export { simpleBrandedNumber } from './branded/simple-branded-number.mjs';
23
23
  export { simpleBrandedString } from './branded/simple-branded-string.mjs';
24
24
  export { intersection } from './compose/intersection.mjs';
25
25
  export { mergeRecords } from './compose/merge-records.mjs';
26
+ export { recursion } from './compose/recursion.mjs';
26
27
  export { union } from './compose/union.mjs';
27
28
  export { enumType } from './enum/enum.mjs';
28
29
  export { uintRange } from './enum/uint-range.mjs';
30
+ export { JsonObject, JsonPrimitive, JsonValue } from './predefined/json.mjs';
29
31
  export { bigint, bigintLiteral } from './primitives/bigint.mjs';
30
32
  export { boolean, booleanLiteral } from './primitives/boolean.mjs';
31
33
  export { nullType } from './primitives/null.mjs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,2 @@
1
+ export * from './json.mjs';
2
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/predefined/index.mts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { JsonObject, JsonPrimitive, JsonValue } from './json.mjs';
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,47 @@
1
+ import { type Type } from '../type.mjs';
2
+ export declare const JsonPrimitive: Readonly<{
3
+ typeName: string;
4
+ defaultValue: string | number | boolean | null;
5
+ is: (a: unknown) => a is string | number | boolean | null;
6
+ assertIs: (a: unknown) => asserts a is string | number | boolean | null;
7
+ cast: (a: unknown) => string | number | boolean | null;
8
+ fill: (a: unknown) => string | number | boolean | null;
9
+ validate: (a: unknown) => import("ts-data-forge").Result<string | number | boolean | null, readonly Readonly<{
10
+ path: readonly string[];
11
+ actualValue: unknown;
12
+ expectedType: string;
13
+ message: string | undefined;
14
+ typeName: string;
15
+ }>[]>;
16
+ optional?: true;
17
+ }>;
18
+ export declare const JsonValue: Type<JsonValue>;
19
+ export declare const JsonObject: Readonly<{
20
+ typeName: string;
21
+ defaultValue: {
22
+ readonly [x: string]: JsonValue;
23
+ };
24
+ is: (a: unknown) => a is {
25
+ readonly [x: string]: JsonValue;
26
+ };
27
+ assertIs: (a: unknown) => asserts a is {
28
+ readonly [x: string]: JsonValue;
29
+ };
30
+ cast: (a: unknown) => {
31
+ readonly [x: string]: JsonValue;
32
+ };
33
+ fill: (a: unknown) => {
34
+ readonly [x: string]: JsonValue;
35
+ };
36
+ validate: (a: unknown) => import("ts-data-forge").Result<{
37
+ readonly [x: string]: JsonValue;
38
+ }, readonly Readonly<{
39
+ path: readonly string[];
40
+ actualValue: unknown;
41
+ expectedType: string;
42
+ message: string | undefined;
43
+ typeName: string;
44
+ }>[]>;
45
+ optional?: true;
46
+ }>;
47
+ //# sourceMappingURL=json.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.mts","sourceRoot":"","sources":["../../src/predefined/json.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,IAAI,EAAe,MAAM,aAAa,CAAC;AAErD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;EAAmD,CAAC;AAW9E,eAAO,MAAM,SAAS,EAAE,IAAI,CAAC,SAAS,CAErC,CAAC;AAYF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAsC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import 'ts-data-forge';
2
+ import { array } from '../array/array.mjs';
3
+ import { recursion } from '../compose/recursion.mjs';
4
+ import { union } from '../compose/union.mjs';
5
+ import { boolean } from '../primitives/boolean.mjs';
6
+ import { nullType } from '../primitives/null.mjs';
7
+ import { number } from '../primitives/number.mjs';
8
+ import { string } from '../primitives/string.mjs';
9
+ import { keyValueRecord } from '../record/key-value-record.mjs';
10
+
11
+ const JsonPrimitive = union([nullType, number(), string(), boolean()]);
12
+ const JsonValue = recursion('JsonValue', () => union([JsonPrimitive, keyValueRecord(string(), JsonValue), array(JsonValue)]));
13
+ const JsonObject = keyValueRecord(string(), JsonValue);
14
+
15
+ export { JsonObject, JsonPrimitive, JsonValue };
16
+ //# sourceMappingURL=json.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.mjs","sources":["../../src/predefined/json.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;;;;MAOa,aAAa,GAAG,KAAK,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC;AAWrE,MAAM,SAAS,GAAoB,SAAS,CAAC,WAAW,EAAE,MAC/D,KAAK,CAAC,CAAC,aAAa,EAAE,cAAc,CAAC,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;AAaxE,MAAM,UAAU,GAAG,cAAc,CAAC,MAAM,EAAE,EAAE,SAAS;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-fortress",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -68,7 +68,7 @@
68
68
  "@semantic-release/github": "^11.0.4",
69
69
  "@semantic-release/npm": "^12.0.2",
70
70
  "@semantic-release/release-notes-generator": "^14.0.3",
71
- "@types/node": "^24.2.1",
71
+ "@types/node": "^24.3.0",
72
72
  "@vitest/coverage-v8": "^3.2.4",
73
73
  "@vitest/ui": "^3.2.4",
74
74
  "conventional-changelog-conventionalcommits": "^9.1.0",
@@ -91,7 +91,7 @@
91
91
  "prettier-plugin-packagejson": "^2.5.19",
92
92
  "rollup": "^4.46.2",
93
93
  "semantic-release": "^24.2.7",
94
- "ts-repo-utils": "^6.0.0",
94
+ "ts-repo-utils": "^6.0.3",
95
95
  "tslib": "^2.8.1",
96
96
  "tsx": "^4.20.4",
97
97
  "typedoc": "^0.28.10",
@@ -1,3 +1,4 @@
1
1
  export * from './intersection.mjs';
2
2
  export * from './merge-records.mjs';
3
+ export * from './recursion.mjs';
3
4
  export * from './union.mjs';
@@ -0,0 +1,49 @@
1
+ import { type Type } from '../type.mjs';
2
+ import { createAssertFn, createCastFn, createIsFn } from '../utils/index.mjs';
3
+
4
+ export const recursion = <A,>(
5
+ typeName: string,
6
+ definition: () => Type<A>,
7
+ options?: Partial<
8
+ Readonly<{
9
+ defaultValue: A;
10
+ }>
11
+ >,
12
+ ): Type<A> => {
13
+ const cache: {
14
+ innerType: Type<A> | undefined;
15
+ computedDefaultValue: A | undefined;
16
+ } = {
17
+ innerType: undefined as Type<A> | undefined,
18
+ computedDefaultValue: undefined as A | undefined,
19
+ };
20
+
21
+ const getInnerType = (): Type<A> => {
22
+ // eslint-disable-next-line functional/immutable-data
23
+ cache.innerType ??= definition();
24
+ return cache.innerType;
25
+ };
26
+
27
+ const validate: Type<A>['validate'] = (a) => getInnerType().validate(a);
28
+
29
+ const is = createIsFn<A>(validate);
30
+
31
+ const fill: Type<A>['fill'] = (a) => getInnerType().fill(a);
32
+
33
+ return {
34
+ typeName,
35
+ get defaultValue(): A {
36
+ if (options?.defaultValue !== undefined) {
37
+ return options.defaultValue;
38
+ }
39
+ // eslint-disable-next-line functional/immutable-data
40
+ cache.computedDefaultValue ??= getInnerType().defaultValue;
41
+ return cache.computedDefaultValue;
42
+ },
43
+ fill,
44
+ validate,
45
+ is,
46
+ assertIs: createAssertFn(validate),
47
+ cast: createCastFn(validate),
48
+ };
49
+ };
@@ -0,0 +1,234 @@
1
+ import { expectType } from 'ts-data-forge';
2
+ import { array } from '../array/index.mjs';
3
+ import { boolean, nullType, number, string } from '../primitives/index.mjs';
4
+ import { keyValueRecord, record } from '../record/index.mjs';
5
+ import { type Type, type TypeOf } from '../type.mjs';
6
+ import { recursion } from './recursion.mjs';
7
+ import { union } from './union.mjs';
8
+
9
+ describe('recursive', () => {
10
+ // Define JsonPrimitive type
11
+ const JsonPrimitive = union([nullType, number(), string(), boolean()]);
12
+
13
+ type JsonPrimitive = TypeOf<typeof JsonPrimitive>;
14
+
15
+ test('JsonPrimitive.defaultValue', () => {
16
+ expectType<JsonPrimitive, null | boolean | number | string>('=');
17
+
18
+ expect(JsonPrimitive.defaultValue).toBeNull();
19
+ });
20
+
21
+ // Define recursive JsonValue type
22
+ type JsonValue =
23
+ | JsonPrimitive
24
+ | Readonly<{
25
+ [k: string]: JsonValue;
26
+ }>
27
+ | readonly JsonValue[];
28
+
29
+ const JsonValue: Type<JsonValue> = recursion('JsonValue', () =>
30
+ union([
31
+ JsonPrimitive,
32
+ keyValueRecord(string(), JsonValue),
33
+ array(JsonValue),
34
+ ]),
35
+ );
36
+
37
+ test('JsonValue.defaultValue', () => {
38
+ expectType<TypeOf<typeof JsonValue>, JsonValue>('=');
39
+
40
+ expect(JsonValue.defaultValue).toBeNull();
41
+ });
42
+
43
+ test('JsonValue - primitive values', () => {
44
+ expect(JsonValue.is(true)).toBe(true);
45
+ expect(JsonValue.is(false)).toBe(true);
46
+ expect(JsonValue.is(42)).toBe(true);
47
+ expect(JsonValue.is(3.14)).toBe(true);
48
+ expect(JsonValue.is('hello')).toBe(true);
49
+ expect(JsonValue.is(null)).toBe(true);
50
+ expect(JsonValue.is(undefined)).toBe(false);
51
+ });
52
+
53
+ test('JsonValue - arrays', () => {
54
+ expect(JsonValue.is([])).toBe(true);
55
+ expect(JsonValue.is([1, 2, 3])).toBe(true);
56
+ expect(JsonValue.is(['a', 'b', 'c'])).toBe(true);
57
+ expect(JsonValue.is([true, false, null])).toBe(true);
58
+ expect(JsonValue.is([1, 'a', true, null])).toBe(true);
59
+ });
60
+
61
+ test('JsonValue - objects', () => {
62
+ expect(JsonValue.is({})).toBe(true);
63
+ expect(JsonValue.is({ a: 1 })).toBe(true);
64
+ expect(JsonValue.is({ a: 1, b: 'hello' })).toBe(true);
65
+ expect(JsonValue.is({ a: 1, b: [1, 2, 3] })).toBe(true);
66
+ });
67
+
68
+ test('JsonValue - nested structures', () => {
69
+ const nestedData = {
70
+ name: 'John',
71
+ age: 30,
72
+ active: true,
73
+ address: {
74
+ street: '123 Main St',
75
+ city: 'New York',
76
+ coordinates: {
77
+ lat: 40.7128,
78
+ lon: -74.006,
79
+ },
80
+ },
81
+ hobbies: ['reading', 'coding', 'gaming'],
82
+ scores: [95, 87, 92],
83
+ metadata: null,
84
+ tags: [],
85
+ settings: {
86
+ theme: 'dark',
87
+ notifications: {
88
+ email: true,
89
+ push: false,
90
+ },
91
+ },
92
+ };
93
+
94
+ expect(JsonValue.is(nestedData)).toBe(true);
95
+ });
96
+
97
+ test('JsonValue - deeply nested arrays', () => {
98
+ const deepArray = [
99
+ [
100
+ [1, 2, 3],
101
+ [4, 5, 6],
102
+ ],
103
+ [
104
+ [7, 8, 9],
105
+ [10, 11, 12],
106
+ ],
107
+ ];
108
+
109
+ expect(JsonValue.is(deepArray)).toBe(true);
110
+ });
111
+
112
+ test('JsonValue - mixed nested structure', () => {
113
+ const mixedData = {
114
+ users: [
115
+ {
116
+ id: 1,
117
+ name: 'Alice',
118
+ tasks: [
119
+ { id: 'task1', completed: true },
120
+ { id: 'task2', completed: false },
121
+ ],
122
+ },
123
+ {
124
+ id: 2,
125
+ name: 'Bob',
126
+ tasks: [],
127
+ },
128
+ ],
129
+ config: {
130
+ version: '1.0.0',
131
+ features: ['feature1', 'feature2'],
132
+ settings: {
133
+ debug: false,
134
+ timeout: 5000,
135
+ },
136
+ },
137
+ };
138
+
139
+ expect(JsonValue.is(mixedData)).toBe(true);
140
+ });
141
+
142
+ test('JsonValue - invalid values', () => {
143
+ expect(JsonValue.is(undefined)).toBe(false);
144
+ expect(JsonValue.is(Symbol('test'))).toBe(false);
145
+ expect(JsonValue.is(() => 'function')).toBe(false);
146
+
147
+ // Note: keyValueRecord accepts any object with string keys, including Date/Set/Map
148
+ // This is expected behavior for keyValueRecord, which doesn't distinguish
149
+ // between plain objects and other object instances
150
+ });
151
+
152
+ test('JsonValue - cast function', () => {
153
+ const result = JsonValue.cast({ a: 1, b: [2, 3], c: { d: 'hello' } });
154
+ expect(result).toStrictEqual({ a: 1, b: [2, 3], c: { d: 'hello' } });
155
+ });
156
+
157
+ test('JsonValue - fill function', () => {
158
+ const result = JsonValue.fill({ a: 1, b: [2, 3] });
159
+ expect(result).toStrictEqual({ a: 1, b: [2, 3] });
160
+
161
+ // Fill with default value for invalid input
162
+ const defaultResult = JsonValue.fill(undefined);
163
+ expect(JsonValue.is(defaultResult)).toBe(true);
164
+ });
165
+
166
+ // Test other recursive structures
167
+ test('Linked list structure', () => {
168
+ type LinkedList<T> = { value: T; next: LinkedList<T> | null };
169
+
170
+ const LinkedListNumber: Type<LinkedList<number>> = recursion(
171
+ 'LinkedList<number>',
172
+ () =>
173
+ record({
174
+ value: number(0),
175
+ next: union([nullType, LinkedListNumber]),
176
+ }),
177
+ { defaultValue: { value: 0, next: null } },
178
+ );
179
+
180
+ const list: LinkedList<number> = {
181
+ value: 1,
182
+ next: {
183
+ value: 2,
184
+ next: {
185
+ value: 3,
186
+ next: null,
187
+ },
188
+ },
189
+ };
190
+
191
+ expect(LinkedListNumber.is(list)).toBe(true);
192
+ expect(LinkedListNumber.is({ value: 1, next: null })).toBe(true);
193
+ expect(LinkedListNumber.is({ value: 'not a number', next: null })).toBe(
194
+ false,
195
+ );
196
+ });
197
+
198
+ test('Tree structure', () => {
199
+ type TreeNode<T> = Readonly<{
200
+ value: T;
201
+ children: readonly TreeNode<T>[];
202
+ }>;
203
+
204
+ const TreeNodeString: Type<TreeNode<string>> = recursion(
205
+ 'TreeNode<string>',
206
+ () =>
207
+ record({
208
+ value: string(),
209
+ children: array(TreeNodeString),
210
+ }),
211
+ );
212
+
213
+ const tree: TreeNode<string> = {
214
+ value: 'root',
215
+ children: [
216
+ {
217
+ value: 'branch1',
218
+ children: [
219
+ { value: 'leaf1', children: [] },
220
+ { value: 'leaf2', children: [] },
221
+ ],
222
+ },
223
+ {
224
+ value: 'branch2',
225
+ children: [{ value: 'leaf3', children: [] }],
226
+ },
227
+ ],
228
+ };
229
+
230
+ expect(TreeNodeString.is(tree)).toBe(true);
231
+ expect(TreeNodeString.is({ value: 'node', children: [] })).toBe(true);
232
+ expect(TreeNodeString.is({ value: 123, children: [] })).toBe(false);
233
+ });
234
+ });
package/src/index.mts CHANGED
@@ -2,6 +2,7 @@ export * from './array/index.mjs';
2
2
  export * from './branded/index.mjs';
3
3
  export * from './compose/index.mjs';
4
4
  export * from './enum/index.mjs';
5
+ export * from './predefined/index.mjs';
5
6
  export * from './primitives/index.mjs';
6
7
  export * from './record/index.mjs';
7
8
  export * from './type.mjs';
@@ -0,0 +1 @@
1
+ export * from './json.mjs';
@@ -0,0 +1,33 @@
1
+ import { expectType } from 'ts-data-forge';
2
+ import { array } from '../array/index.mjs';
3
+ import { recursion, union } from '../compose/index.mjs';
4
+ import { boolean, nullType, number, string } from '../primitives/index.mjs';
5
+ import { keyValueRecord } from '../record/index.mjs';
6
+ import { type Type, type TypeOf } from '../type.mjs';
7
+
8
+ export const JsonPrimitive = union([nullType, number(), string(), boolean()]);
9
+
10
+ if (import.meta.vitest !== undefined) {
11
+ test('JsonPrimitive', () => {
12
+ expectType<TypeOf<typeof JsonPrimitive>, JsonPrimitive>('=');
13
+ expectType<JsonPrimitive, null | boolean | number | string>('=');
14
+
15
+ expect(JsonPrimitive.defaultValue).toBeNull();
16
+ });
17
+ }
18
+
19
+ export const JsonValue: Type<JsonValue> = recursion('JsonValue', () =>
20
+ union([JsonPrimitive, keyValueRecord(string(), JsonValue), array(JsonValue)]),
21
+ );
22
+
23
+ if (import.meta.vitest !== undefined) {
24
+ test('JsonValue', () => {
25
+ expectType<TypeOf<typeof JsonValue>, JsonValue>('=');
26
+
27
+ expect(JsonValue.defaultValue).toBeNull();
28
+ });
29
+ }
30
+
31
+ type JsonObject = ReadonlyRecord<string, JsonValue>;
32
+
33
+ export const JsonObject = keyValueRecord(string(), JsonValue);