@kemdict/json-to-ts 0.1.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.
@@ -0,0 +1,308 @@
1
+ import { hash } from "node:crypto";
2
+
3
+ import type { TypeGroup, TypeDescription, TypeStructure } from "./model.ts";
4
+ import { isHash, getTypeDescriptionGroup, findTypeById, isArray, isObject, onlyUnique, isDate } from "./util.ts";
5
+
6
+ function createTypeDescription(typeObj: any | string[], isUnion: boolean): TypeDescription {
7
+ if (isArray(typeObj)) {
8
+ return {
9
+ id: Hash(JSON.stringify([...typeObj, isUnion])),
10
+ arrayOfTypes: typeObj,
11
+ isUnion,
12
+ };
13
+ } else {
14
+ return {
15
+ id: Hash(JSON.stringify(typeObj)),
16
+ typeObj,
17
+ };
18
+ }
19
+ }
20
+
21
+ function getIdByType(typeObj: any | string[], types: TypeDescription[], isUnion: boolean = false): string {
22
+ let typeDesc = types.find((el) => {
23
+ return typeObjectMatchesTypeDesc(typeObj, el, isUnion);
24
+ });
25
+
26
+ if (!typeDesc) {
27
+ typeDesc = createTypeDescription(typeObj, isUnion);
28
+ types.push(typeDesc);
29
+ }
30
+
31
+ return typeDesc.id;
32
+ }
33
+
34
+ function Hash(content: string): string {
35
+ return hash("sha1", content);
36
+ }
37
+
38
+ function typeObjectMatchesTypeDesc(typeObj: any | string[], typeDesc: TypeDescription, isUnion: boolean): boolean {
39
+ if (isArray(typeObj)) {
40
+ return arraysContainSameElements(typeObj, typeDesc.arrayOfTypes!) && typeDesc.isUnion === isUnion;
41
+ } else {
42
+ return objectsHaveSameEntries(typeObj, typeDesc.typeObj);
43
+ }
44
+ }
45
+
46
+ function arraysContainSameElements(arr1: any[], arr2: any[]): boolean {
47
+ if (arr1 === undefined || arr2 === undefined) return false;
48
+
49
+ return arr1.sort().join("") === arr2.sort().join("");
50
+ }
51
+
52
+ function objectsHaveSameEntries(obj1: any, obj2: any): boolean {
53
+ if (obj1 === undefined || obj2 === undefined) return false;
54
+
55
+ const entries1 = Object.entries(obj1);
56
+ const entries2 = Object.entries(obj2);
57
+
58
+ const sameLength = entries1.length === entries2.length;
59
+
60
+ const sameTypes = entries1.every(([key, value]) => {
61
+ return obj2[key] === value;
62
+ });
63
+
64
+ return sameLength && sameTypes;
65
+ }
66
+
67
+ function getSimpleTypeName(value: any): string {
68
+ if (value === null) {
69
+ return "null";
70
+ } else if (value instanceof Date) {
71
+ return "Date";
72
+ } else {
73
+ return typeof value;
74
+ }
75
+ }
76
+
77
+ function getTypeGroup(value: any): TypeGroup {
78
+ if (isDate(value)) {
79
+ return "date";
80
+ } else if (isArray(value)) {
81
+ return "array";
82
+ } else if (isObject(value)) {
83
+ return "object";
84
+ } else {
85
+ return "primitive";
86
+ }
87
+ }
88
+
89
+ function createTypeObject(obj: any, types: TypeDescription[]): any {
90
+ return Object.entries(obj).reduce((typeObj, [key, value]) => {
91
+ const { rootTypeId } = getTypeStructure(value, types);
92
+
93
+ return {
94
+ ...typeObj,
95
+ [key]: rootTypeId,
96
+ };
97
+ }, {});
98
+ }
99
+
100
+ function getMergedObjects(typesOfArray: TypeDescription[], types: TypeDescription[]): string {
101
+ const typeObjects = typesOfArray.map((typeDesc) => typeDesc.typeObj);
102
+
103
+ const allKeys = typeObjects
104
+ .map((typeObj) => Object.keys(typeObj!))
105
+ .reduce((a, b) => [...a, ...b], [])
106
+ .filter(onlyUnique);
107
+
108
+ const commonKeys = typeObjects.reduce((commonKeys: string[], typeObj) => {
109
+ const keys = Object.keys(typeObj!);
110
+ return commonKeys.filter((key) => keys.includes(key));
111
+ }, allKeys) as string[];
112
+
113
+ const getKeyType = (key: string) => {
114
+ const typesOfKey = typeObjects
115
+ .filter((typeObj) => {
116
+ return Object.keys(typeObj!).includes(key);
117
+ })
118
+ .map((typeObj) => typeObj![key])
119
+ .filter(onlyUnique);
120
+
121
+ if (typesOfKey.length === 1) {
122
+ return typesOfKey.pop();
123
+ } else {
124
+ return getInnerArrayType(typesOfKey, types);
125
+ }
126
+ };
127
+
128
+ const typeObj = allKeys.reduce((obj: object, key: string) => {
129
+ const isMandatory = commonKeys.includes(key);
130
+ const type = getKeyType(key);
131
+
132
+ const keyValue = isMandatory ? key : toOptionalKey(key);
133
+
134
+ return {
135
+ ...obj,
136
+ [keyValue]: type,
137
+ };
138
+ }, {});
139
+ return getIdByType(typeObj, types, true);
140
+ }
141
+
142
+ function toOptionalKey(key: string): string {
143
+ return key.endsWith("--?") ? key : `${key}--?`;
144
+ }
145
+
146
+ function getMergedArrays(typesOfArray: TypeDescription[], types: TypeDescription[]): string {
147
+ const idsOfArrayTypes = typesOfArray
148
+ .map((typeDesc) => typeDesc.arrayOfTypes!)
149
+ .reduce((a, b) => [...a, ...b], [])
150
+ .filter(onlyUnique);
151
+
152
+ if (idsOfArrayTypes.length === 1) {
153
+ return getIdByType([idsOfArrayTypes.pop()], types);
154
+ } else {
155
+ return getIdByType([getInnerArrayType(idsOfArrayTypes, types)], types);
156
+ }
157
+ }
158
+
159
+ // we merge union types example: (number | string), null -> (number | string | null)
160
+ function getMergedUnion(typesOfArray: string[], types: TypeDescription[]): string {
161
+ const innerUnionsTypes = typesOfArray
162
+ .map((id) => {
163
+ return findTypeById(id, types);
164
+ })
165
+ .filter((_) => !!_ && _.isUnion)
166
+ .map((_) => _!.arrayOfTypes!)
167
+ .reduce((a, b) => [...a, ...b], []);
168
+
169
+ const primitiveTypes = typesOfArray.filter((id) => !findTypeById(id, types) || !findTypeById(id, types)?.isUnion); // primitives or not union
170
+ return getIdByType([...innerUnionsTypes, ...primitiveTypes], types, true);
171
+ }
172
+
173
+ function getInnerArrayType(typesOfArray: string[], types: TypeDescription[]): string {
174
+ // return inner array type
175
+
176
+ const containsUndefined = typesOfArray.includes("undefined");
177
+
178
+ const arrayTypesDescriptions = typesOfArray.map((id) => findTypeById(id, types)).filter((_) => !!_);
179
+
180
+ const allArrayType =
181
+ arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc).group === "array").length ===
182
+ typesOfArray.length;
183
+
184
+ const allArrayTypeWithUndefined =
185
+ arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc).group === "array").length + 1 ===
186
+ typesOfArray.length && containsUndefined;
187
+
188
+ const allObjectTypeWithUndefined =
189
+ arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc).group === "object").length + 1 ===
190
+ typesOfArray.length && containsUndefined;
191
+
192
+ const allObjectType =
193
+ arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc).group === "object").length ===
194
+ typesOfArray.length;
195
+
196
+ if (typesOfArray.length === 0) {
197
+ // no types in array -> empty union type
198
+ return getIdByType([], types, true);
199
+ }
200
+
201
+ if (typesOfArray.length === 1) {
202
+ // one type in array -> that will be our inner type
203
+ return typesOfArray.pop() as (typeof typesOfArray)[number];
204
+ }
205
+
206
+ if (typesOfArray.length > 1) {
207
+ // multiple types in merge array
208
+ // if all are object we can merge them and return merged object as inner type
209
+ if (allObjectType) return getMergedObjects(arrayTypesDescriptions, types);
210
+ // if all are array we can merge them and return merged array as inner type
211
+ if (allArrayType) return getMergedArrays(arrayTypesDescriptions, types);
212
+
213
+ // all array types with posibble undefined, result type = undefined | (*mergedArray*)[]
214
+ if (allArrayTypeWithUndefined) {
215
+ return getMergedUnion([getMergedArrays(arrayTypesDescriptions, types), "undefined"], types);
216
+ }
217
+
218
+ // all object types with posibble undefined, result type = undefined | *mergedObject*
219
+ if (allObjectTypeWithUndefined) {
220
+ return getMergedUnion([getMergedObjects(arrayTypesDescriptions, types), "undefined"], types);
221
+ }
222
+
223
+ // if they are mixed or all primitive we cant merge them so we return as mixed union type
224
+ return getMergedUnion(typesOfArray, types);
225
+ }
226
+ throw new Error("Failed to get inner array type");
227
+ }
228
+
229
+ export function getTypeStructure(
230
+ targetObj: any, // object that we want to create types for
231
+ types: TypeDescription[] = []
232
+ ): TypeStructure {
233
+ switch (getTypeGroup(targetObj)) {
234
+ case "array":
235
+ const typesOfArray = (targetObj as any[]).map((_) => getTypeStructure(_, types).rootTypeId).filter(onlyUnique);
236
+ const arrayInnerTypeId = getInnerArrayType(typesOfArray, types); // create "union type of array types"
237
+ const typeId = getIdByType([arrayInnerTypeId], types); // create type "array of union type"
238
+
239
+ return {
240
+ rootTypeId: typeId,
241
+ types,
242
+ };
243
+
244
+ case "object":
245
+ const typeObj = createTypeObject(targetObj, types);
246
+ const objType = getIdByType(typeObj, types);
247
+
248
+ return {
249
+ rootTypeId: objType,
250
+ types,
251
+ };
252
+
253
+ case "primitive":
254
+ return {
255
+ rootTypeId: getSimpleTypeName(targetObj),
256
+ types,
257
+ };
258
+
259
+ case "date":
260
+ const dateType = getSimpleTypeName(targetObj);
261
+
262
+ return {
263
+ rootTypeId: dateType,
264
+ types,
265
+ };
266
+ }
267
+ }
268
+
269
+ function getAllUsedTypeIds({ rootTypeId, types }: TypeStructure): string[] | undefined {
270
+ const typeDesc = types.find((_) => _.id === rootTypeId);
271
+
272
+ function subTypes(typeDesc: TypeDescription | undefined): string[] | undefined {
273
+ const { group, desc } = getTypeDescriptionGroup(typeDesc);
274
+ switch (group) {
275
+ case "array":
276
+ const arrSubTypes = desc.arrayOfTypes
277
+ .filter(isHash)
278
+ .map((typeId) => {
279
+ const typeDesc = types.find((_) => _.id === typeId);
280
+ return subTypes(typeDesc);
281
+ })
282
+ .reduce((a, b) => [...a!, ...b!], []);
283
+ return [desc.id, ...arrSubTypes!];
284
+
285
+ case "object":
286
+ const objSubTypes = Object.values(desc.typeObj)
287
+ .filter(isHash)
288
+ .map((typeId) => {
289
+ const typeDesc = types.find((_) => _.id === typeId);
290
+ return subTypes(typeDesc);
291
+ })
292
+ .reduce((a, b) => [...a!, ...b!], []);
293
+ return [desc.id, ...objSubTypes!];
294
+ }
295
+ }
296
+
297
+ const result = subTypes(typeDesc);
298
+ if (!result) throw new Error("Failed to get all used type Ids");
299
+ return result;
300
+ }
301
+
302
+ export function optimizeTypeStructure(typeStructure: TypeStructure) {
303
+ const usedTypeIds = getAllUsedTypeIds(typeStructure);
304
+
305
+ const optimizedTypes = typeStructure.types.filter((typeDesc) => usedTypeIds?.includes(typeDesc.id));
306
+
307
+ typeStructure.types = optimizedTypes;
308
+ }
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { getTypeStructure, optimizeTypeStructure } from "./get-type-structure.ts";
2
+ import type { Options, State } from "./model.ts";
3
+ import { getInterfaceDescriptions, getInterfaceStringFromDescription } from "./get-interfaces.ts";
4
+ import { getNames } from "./get-names.ts";
5
+ import { isArray, isObject } from "./util.ts";
6
+
7
+ export function JsonToTS(json: any, options?: Options): string[] {
8
+ const state = {
9
+ keyName: options?.rootName ?? "RootObject",
10
+ export: !!options?.export,
11
+ useTypeAlias: !!options?.useTypeAlias,
12
+ lvl: 0,
13
+ } satisfies State;
14
+
15
+ /**
16
+ * Parsing currently works with (Objects) and (Array of Objects) not and primitive types and mixed arrays etc..
17
+ * so we shall validate, so we dont start parsing non Object type
18
+ */
19
+ const isArrayOfObjects = isArray(json) && json.length > 0 && json.reduce((a: any, b: any) => a && isObject(b), true);
20
+
21
+ if (!(isObject(json) || isArrayOfObjects)) {
22
+ throw new Error("Only (Object) and (Array of Object) are supported");
23
+ }
24
+
25
+ const typeStructure = getTypeStructure(json);
26
+ /**
27
+ * due to merging array types some types are switched out for merged ones
28
+ * so we delete the unused ones here
29
+ */
30
+ optimizeTypeStructure(typeStructure);
31
+
32
+ const names = getNames(typeStructure, state);
33
+
34
+ return getInterfaceDescriptions(typeStructure, names).map((description) =>
35
+ getInterfaceStringFromDescription({ ...description, state })
36
+ );
37
+ }
38
+
39
+ export default JsonToTS;
package/src/model.ts ADDED
@@ -0,0 +1,65 @@
1
+ export type TypeGroup = "primitive" | "array" | "object" | "date";
2
+
3
+ export type TypeGroupWithDescription =
4
+ | {
5
+ group: "primitive";
6
+ desc: undefined;
7
+ }
8
+ | { group: "array"; desc: TypeDescriptionWithArrayOfTypes }
9
+ | { group: "object"; desc: TypeDescriptionWithTypeObj };
10
+
11
+ export interface TypeDescription {
12
+ id: string;
13
+ isUnion?: boolean;
14
+ typeObj?: { [index: string]: string };
15
+ arrayOfTypes?: string[];
16
+ }
17
+
18
+ export interface TypeDescriptionWithArrayOfTypes extends TypeDescription {
19
+ arrayOfTypes: string[];
20
+ }
21
+ export interface TypeDescriptionWithTypeObj extends TypeDescription {
22
+ typeObj: { [index: string]: string };
23
+ }
24
+
25
+ export interface TypeStructure {
26
+ rootTypeId: string;
27
+ types: TypeDescription[];
28
+ }
29
+
30
+ export interface NameEntry {
31
+ id: string;
32
+ name: string;
33
+ }
34
+
35
+ export interface NameStructure {
36
+ rootName: string;
37
+ names: NameEntry[];
38
+ }
39
+
40
+ export interface InterfaceDescription {
41
+ name: string;
42
+ typeMap: object;
43
+ }
44
+
45
+ export interface Options {
46
+ /** The name of the generated root type */
47
+ rootName?: string;
48
+ /** Whether to export the root type. */
49
+ export?: boolean;
50
+ /** Whether to generate `type Foo = { ... }` instead of interface */
51
+ useTypeAlias?: boolean;
52
+ }
53
+
54
+ export interface State {
55
+ keyName: string;
56
+ export: boolean;
57
+ useTypeAlias: boolean;
58
+ // Nested level (0+ integer), used to make sure we don't singularize the root object
59
+ lvl: number;
60
+ }
61
+
62
+ export interface KeyMetaData {
63
+ keyValue: string;
64
+ isOptional: boolean;
65
+ }
package/src/util.ts ADDED
@@ -0,0 +1,63 @@
1
+ import type {
2
+ KeyMetaData,
3
+ TypeDescription,
4
+ TypeDescriptionWithArrayOfTypes,
5
+ TypeGroupWithDescription,
6
+ TypeDescriptionWithTypeObj,
7
+ } from "./model.ts";
8
+
9
+ export function isHash(str: string) {
10
+ return str.length === 40;
11
+ }
12
+
13
+ export function onlyUnique<T>(value: T, index: number, self: T[]) {
14
+ return self.indexOf(value) === index;
15
+ }
16
+
17
+ export function isArray(x: any) {
18
+ return Object.prototype.toString.call(x) === "[object Array]";
19
+ }
20
+
21
+ export function isNonArrayUnion(typeName: string) {
22
+ const arrayUnionRegex = /^\(.*\)\[\]$/;
23
+
24
+ return typeName.includes(" | ") && !arrayUnionRegex.test(typeName);
25
+ }
26
+
27
+ export function isObject(x: any) {
28
+ return Object.prototype.toString.call(x) === "[object Object]" && x !== null;
29
+ }
30
+
31
+ export function isDate(x: any) {
32
+ return x instanceof Date;
33
+ }
34
+
35
+ export function parseKeyMetaData(key: string): KeyMetaData {
36
+ const isOptional = key.endsWith("--?");
37
+
38
+ if (isOptional) {
39
+ return {
40
+ isOptional,
41
+ keyValue: key.slice(0, -3),
42
+ };
43
+ } else {
44
+ return {
45
+ isOptional,
46
+ keyValue: key,
47
+ };
48
+ }
49
+ }
50
+
51
+ export function getTypeDescriptionGroup(desc: TypeDescription | undefined): TypeGroupWithDescription {
52
+ if (desc === undefined) {
53
+ return { group: "primitive", desc };
54
+ } else if (desc.arrayOfTypes !== undefined) {
55
+ return { group: "array", desc: desc as TypeDescriptionWithArrayOfTypes };
56
+ } else {
57
+ return { group: "object", desc: desc as TypeDescriptionWithTypeObj };
58
+ }
59
+ }
60
+
61
+ export function findTypeById(id: string, types: TypeDescription[]): TypeDescription | undefined {
62
+ return types.find((_) => _.id === id);
63
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["*"],
4
+ "module": "nodenext",
5
+ "target": "esnext",
6
+ "strict": true,
7
+ "sourceMap": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "allowImportingTsExtensions": true,
11
+ "rewriteRelativeImportExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "noEmit": true
14
+ }
15
+ }