@fgv/ts-json-base 5.0.0-26 → 5.0.0-27

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.
package/README.md CHANGED
@@ -32,3 +32,153 @@ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
32
32
  interface JsonArray extends Array<JsonValue> { }
33
33
  ```
34
34
 
35
+ ### JsonCompatible Type
36
+
37
+ The `JsonCompatible<T>` type provides compile-time validation that a type is JSON-serializable, unlike `JsonValue` which requires an index signature. This is particularly useful for strongly-typed interfaces.
38
+
39
+ #### Basic Usage
40
+
41
+ ```typescript
42
+ import { JsonCompatible } from '@fgv/ts-json-base';
43
+
44
+ // ✅ JSON-compatible interface
45
+ interface UserData {
46
+ id: string;
47
+ name: string;
48
+ preferences: {
49
+ theme: 'light' | 'dark';
50
+ notifications: boolean;
51
+ };
52
+ }
53
+
54
+ // ✅ This type remains unchanged because it's fully JSON-compatible
55
+ type ValidatedUserData = JsonCompatible<UserData>;
56
+
57
+ // ❌ Interface with non-JSON properties
58
+ interface UserWithMethods {
59
+ id: string;
60
+ name: string;
61
+ save(): Promise<void>; // Functions are not JSON-serializable
62
+ }
63
+
64
+ // ❌ This type transforms 'save' to an error type
65
+ type InvalidUserData = JsonCompatible<UserWithMethods>;
66
+ // Result: { id: string; name: string; save: ['Error: Function is not JSON-compatible'] }
67
+ ```
68
+
69
+ #### The MapOf Pattern
70
+
71
+ The most powerful application is using `JsonCompatible<T>` as a constraint with default type parameters:
72
+
73
+ ```typescript
74
+ class MapOf<T, TV extends JsonCompatible<T> = JsonCompatible<T>> extends Map<string, TV> {
75
+ public override set(key: string, value: TV): this {
76
+ return super.set(key, value);
77
+ }
78
+ }
79
+
80
+ // ✅ Works perfectly with JSON-compatible types
81
+ const userMap = new MapOf<UserData>();
82
+ userMap.set('user1', {
83
+ id: 'user1',
84
+ name: 'Alice',
85
+ preferences: { theme: 'dark', notifications: true }
86
+ });
87
+
88
+ // ❌ Prevents usage with non-JSON-compatible types
89
+ const badMap = new MapOf<UserWithMethods>();
90
+ // The 'save' method becomes type 'never', making the interface unusable
91
+ ```
92
+
93
+ #### Real-World Examples
94
+
95
+ **API Response Caching:**
96
+ ```typescript
97
+ interface ApiResponse {
98
+ id: string;
99
+ data: unknown;
100
+ timestamp: number;
101
+ metadata: Record<string, string | number>;
102
+ }
103
+
104
+ class ApiCache extends MapOf<ApiResponse> {
105
+ setWithTTL(key: string, response: ApiResponse, ttlMs: number) {
106
+ this.set(key, response);
107
+ setTimeout(() => this.delete(key), ttlMs);
108
+ }
109
+ }
110
+ ```
111
+
112
+ **Configuration Management:**
113
+ ```typescript
114
+ interface AppConfig {
115
+ features: Record<string, boolean>;
116
+ limits: {
117
+ maxUsers: number;
118
+ maxStorage: number;
119
+ };
120
+ ui: {
121
+ theme: 'light' | 'dark';
122
+ language: string;
123
+ };
124
+ }
125
+
126
+ // Ensures config is always serializable for storage/transmission
127
+ const configStore = new MapOf<AppConfig>();
128
+ ```
129
+
130
+ **Data Transfer Objects:**
131
+ ```typescript
132
+ // ✅ Perfect for DTOs - no methods, just data
133
+ interface CreateUserRequest {
134
+ name: string;
135
+ email: string;
136
+ preferences?: {
137
+ newsletter: boolean;
138
+ theme: string;
139
+ };
140
+ }
141
+
142
+ const requestCache = new MapOf<CreateUserRequest>();
143
+
144
+ // ❌ Domain objects with behavior are rejected
145
+ interface User extends CreateUserRequest {
146
+ id: string;
147
+ save(): Promise<void>;
148
+ validate(): boolean;
149
+ }
150
+
151
+ // This won't work - methods become 'never'
152
+ // const userStore = new MapOf<User>();
153
+ ```
154
+
155
+ #### Array and Nested Validation
156
+
157
+ ```typescript
158
+ // ✅ Arrays of JSON-compatible types work fine
159
+ interface DataStructure {
160
+ numbers: number[];
161
+ objects: Array<{ id: number; name: string }>;
162
+ matrix: number[][];
163
+ }
164
+
165
+ const dataMap = new MapOf<DataStructure>();
166
+
167
+ // ❌ Arrays with functions are detected
168
+ interface WithHandlers {
169
+ callbacks: Array<() => void>; // Functions in arrays also become 'never'
170
+ data: string[];
171
+ }
172
+
173
+ // The 'callbacks' property becomes Array<['Error: Function is not JSON-compatible']>
174
+ const handlersMap = new MapOf<WithHandlers>();
175
+ ```
176
+
177
+ #### Benefits
178
+
179
+ 1. **Compile-time safety**: Non-serializable types are caught during development
180
+ 2. **Clear interfaces**: Makes it obvious which types are meant for serialization
181
+ 3. **Architecture enforcement**: Separates data objects from behavior objects
182
+ 4. **Runtime protection**: Prevents serialization errors in production
183
+ 5. **IDE support**: Full autocomplete and error highlighting
184
+
@@ -153,8 +153,8 @@ export declare function isJsonArray(from: unknown): from is JsonArray;
153
153
  /**
154
154
  * Test if an `unknown` is potentially a {@link JsonObject | JsonObject}.
155
155
  * @param from - The `unknown` to be tested.
156
- * @returns `true` if the supplied parameter is a non-array, non-special object,
157
- * `false` otherwise.
156
+ * @returns `true` if the supplied parameter is a non-array, non-special object
157
+ * with no symbol keys, `false` otherwise.
158
158
  * @public
159
159
  */
160
160
  export declare function isJsonObject(from: unknown): from is JsonObject;
@@ -202,6 +202,24 @@ declare const jsonArray: Converter<JsonArray, IJsonConverterContext>;
202
202
  */
203
203
  declare const jsonArray_2: Validator<JsonArray, IJsonValidatorContext>;
204
204
 
205
+ /**
206
+ * A constrained type that is compatible with JSON serialization.
207
+ * @param T - The type to be constrained
208
+ * @returns A constrained type that is compatible with JSON serialization.
209
+ * @public
210
+ */
211
+ export declare type JsonCompatible<T> = T extends JsonPrimitive ? T : T extends Array<unknown> ? JsonCompatibleArray<T[number]> : T extends Function ? ['Error: Function is not JSON-compatible'] : T extends object ? {
212
+ [K in keyof T]: JsonCompatible<T[K]>;
213
+ } : ['Error: Non-JSON type'];
214
+
215
+ /**
216
+ * A type that represents an array of JSON-compatible values.
217
+ * @param T - The type to be constrained
218
+ * @returns A constrained type that is compatible with JSON serialization.
219
+ * @public
220
+ */
221
+ export declare type JsonCompatibleArray<T> = Array<JsonCompatible<T>>;
222
+
205
223
  declare namespace JsonFile {
206
224
  export {
207
225
  readJsonFileSync,
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.52.11"
8
+ "packageVersion": "7.52.12"
9
9
  }
10
10
  ]
11
11
  }
@@ -29,6 +29,22 @@ export interface JsonArray extends Array<JsonValue> {
29
29
  * @public
30
30
  */
31
31
  export type JsonValueType = 'primitive' | 'object' | 'array';
32
+ /**
33
+ * A constrained type that is compatible with JSON serialization.
34
+ * @param T - The type to be constrained
35
+ * @returns A constrained type that is compatible with JSON serialization.
36
+ * @public
37
+ */
38
+ export type JsonCompatible<T> = T extends JsonPrimitive ? T : T extends Array<unknown> ? JsonCompatibleArray<T[number]> : T extends Function ? ['Error: Function is not JSON-compatible'] : T extends object ? {
39
+ [K in keyof T]: JsonCompatible<T[K]>;
40
+ } : ['Error: Non-JSON type'];
41
+ /**
42
+ * A type that represents an array of JSON-compatible values.
43
+ * @param T - The type to be constrained
44
+ * @returns A constrained type that is compatible with JSON serialization.
45
+ * @public
46
+ */
47
+ export type JsonCompatibleArray<T> = Array<JsonCompatible<T>>;
32
48
  /**
33
49
  * Test if an `unknown` is a {@link JsonValue | JsonValue}.
34
50
  * @param from - The `unknown` to be tested
@@ -40,8 +56,8 @@ export declare function isJsonPrimitive(from: unknown): from is JsonPrimitive;
40
56
  /**
41
57
  * Test if an `unknown` is potentially a {@link JsonObject | JsonObject}.
42
58
  * @param from - The `unknown` to be tested.
43
- * @returns `true` if the supplied parameter is a non-array, non-special object,
44
- * `false` otherwise.
59
+ * @returns `true` if the supplied parameter is a non-array, non-special object
60
+ * with no symbol keys, `false` otherwise.
45
61
  * @public
46
62
  */
47
63
  export declare function isJsonObject(from: unknown): from is JsonObject;
@@ -43,8 +43,8 @@ function isJsonPrimitive(from) {
43
43
  /**
44
44
  * Test if an `unknown` is potentially a {@link JsonObject | JsonObject}.
45
45
  * @param from - The `unknown` to be tested.
46
- * @returns `true` if the supplied parameter is a non-array, non-special object,
47
- * `false` otherwise.
46
+ * @returns `true` if the supplied parameter is a non-array, non-special object
47
+ * with no symbol keys, `false` otherwise.
48
48
  * @public
49
49
  */
50
50
  function isJsonObject(from) {
@@ -52,7 +52,11 @@ function isJsonObject(from) {
52
52
  from !== null &&
53
53
  !Array.isArray(from) &&
54
54
  !(from instanceof RegExp) &&
55
- !(from instanceof Date));
55
+ !(from instanceof Date) &&
56
+ !(from instanceof Map) &&
57
+ !(from instanceof Set) &&
58
+ !(from instanceof Error) &&
59
+ Object.getOwnPropertySymbols(from).length === 0);
56
60
  }
57
61
  /**
58
62
  * Test if an `unknown` is potentially a {@link JsonArray | JsonArray}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-json-base",
3
- "version": "5.0.0-26",
3
+ "version": "5.0.0-27",
4
4
  "description": "Typescript types and basic functions for working with json",
5
5
  "main": "lib/index.js",
6
6
  "types": "dist/ts-json-base.d.ts",
@@ -31,20 +31,20 @@
31
31
  "ts-node": "^10.9.2",
32
32
  "typescript": "5.8.3",
33
33
  "eslint-plugin-n": "^17.21.3",
34
- "@rushstack/heft-node-rig": "2.9.4",
35
- "@rushstack/heft": "0.74.3",
36
- "@rushstack/heft-jest-plugin": "0.16.12",
34
+ "@rushstack/heft-node-rig": "2.9.5",
35
+ "@rushstack/heft": "0.74.4",
36
+ "@rushstack/heft-jest-plugin": "0.16.13",
37
37
  "@types/heft-jest": "1.0.6",
38
38
  "@microsoft/api-documenter": "^7.26.31",
39
39
  "@rushstack/eslint-patch": "~1.12.0",
40
40
  "@rushstack/eslint-config": "4.4.0",
41
41
  "eslint-plugin-tsdoc": "~0.4.0",
42
- "@fgv/ts-utils": "5.0.0-26",
43
- "@fgv/ts-extras": "5.0.0-26",
44
- "@fgv/ts-utils-jest": "5.0.0-26"
42
+ "@fgv/ts-utils-jest": "5.0.0-27",
43
+ "@fgv/ts-extras": "5.0.0-27",
44
+ "@fgv/ts-utils": "5.0.0-27"
45
45
  },
46
46
  "peerDependencies": {
47
- "@fgv/ts-utils": "5.0.0-26"
47
+ "@fgv/ts-utils": "5.0.0-27"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "heft test --clean",