forge-type 1.0.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.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # TypeForge
2
+
3
+ TypeForge is a TypeScript utility library that **automatically generates types/interfaces** from backend JSON data.
4
+ It also supports **enum detection** for short string fields and helps speed up frontend development by reducing boilerplate code.
5
+
6
+ ## Features
7
+
8
+ - Automatically generate TypeScript interfaces from any JSON data
9
+ - Detects enums for fields like `gender`, `role`, `status`, and more
10
+ - Handles nested objects and arrays
11
+ - Optional modification of existing types
12
+ - Clean, maintainable code generation for frontend teams
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # Scoped package (public)
18
+ npm install type-forge-lib
19
+
20
+ import { createTypeFromData } from 'type-forge-lib'
21
+
22
+ const userData = {
23
+ id: 1,
24
+ name: 'John Doe',
25
+ gender: 'male',
26
+ role: 'admin',
27
+ status: 'active',
28
+ friends: [
29
+ {
30
+ id: 2,
31
+ name: 'Jane Smith',
32
+ age: '24',
33
+ gender: 'female',
34
+ city: 'many'
35
+ }
36
+ ],
37
+ profile: {
38
+ bio: 'I love art',
39
+ website: 'https://example.com'
40
+ }
41
+ }
42
+
43
+ // Generate or update a TypeScript interface
44
+ const result = createTypeFromData('User', userData, { modify: true })
45
+
46
+ console.log('--- Generated Type ---')
47
+ console.log(result.type)
48
+
49
+ console.log('--- File Path ---')
50
+ console.log(result.filePath)
51
+
52
+ export interface User {
53
+ id: number;
54
+ name: string;
55
+ gender: "male" | "female";
56
+ role: "admin" | "user";
57
+ status: "active" | "inactive";
58
+ friends: Friend[];
59
+ profile: { bio: string; website: string };
60
+ }
61
+
62
+ export interface Friend {
63
+ id: number;
64
+ name: string;
65
+ age: string;
66
+ gender: "male" | "female";
67
+ city: string;
68
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "forge-type",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "dev": "ts-node src/index.ts",
9
+ "test": "ts-node src/test/test.ts",
10
+ "clean": "rm -rf dist types/generated"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC",
15
+ "dependencies": {
16
+ "@types/node": "^25.0.3",
17
+ "ts-node": "^10.9.2",
18
+ "typescript": "^5.9.3"
19
+ }
20
+ }
@@ -0,0 +1,121 @@
1
+ import { inferType } from '../infer/inferType'
2
+ import { saveTypeFile, readTypeFile } from '../storage/fileWriter'
3
+ import { capitalize, singularize } from '../utils/opt-func'
4
+
5
+ const generatedInterfaces = new Set<string>()
6
+ const generatedEnums = new Set<string>()
7
+ const fieldValuesMap: Record<string, Set<string>> = {}
8
+
9
+ // Fields allowed as enums
10
+ const allowedEnumFields = [
11
+ 'gender', 'role', 'status', 'accountType', 'subscription', 'membership', 'accessLevel',
12
+ 'visibility', 'type', 'priority', 'category',
13
+ 'paymentMethod', 'paymentStatus', 'deliveryStatus',
14
+ 'level', 'mode', 'subscriptionStatus', 'confirmation'
15
+ ]
16
+
17
+ export function generateInterface(
18
+ name: string,
19
+ data: any,
20
+ options?: { modify?: boolean }
21
+ ): string {
22
+ const allInterfaces: string[] = []
23
+ const enumDefinitions: string[] = []
24
+
25
+ function helper(name: string, data: any) {
26
+ let existingProps: Record<string, string> = {}
27
+
28
+ if (options?.modify) {
29
+ const existingContent = readTypeFile(name)
30
+ if (existingContent) existingProps = parseInterface(existingContent)
31
+ }
32
+
33
+ const bodyLines: string[] = []
34
+
35
+ Object.entries(data).forEach(([key, value]) => {
36
+ let typeStr = inferType(value, key)
37
+ const optional = value === null || value === undefined ? '?' : ''
38
+
39
+ // Handle nested arrays of objects
40
+ if (Array.isArray(value) && typeof value[0] === 'object' && value[0] !== null) {
41
+ const typeName = capitalize(singularize(key))
42
+ if (!generatedInterfaces.has(typeName)) {
43
+ generatedInterfaces.add(typeName)
44
+ helper(typeName, value[0])
45
+
46
+ Object.entries(value[0]).forEach(([k, v]) => {
47
+ if (typeof v === 'string') detectEnum(k, v, enumDefinitions)
48
+ })
49
+ }
50
+ typeStr = `${typeName}[]`
51
+ }
52
+
53
+ // Handle nested objects
54
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
55
+ typeStr = `{ ${Object.entries(value)
56
+ .map(([k, v]) => {
57
+ if (typeof v === 'string') detectEnum(k, v, enumDefinitions)
58
+ return `${k}: ${inferType(v, k)};`
59
+ })
60
+ .join(' ')} }`
61
+ }
62
+
63
+ // Top-level enums
64
+ if (typeof value === 'string') {
65
+ const enumType = detectEnum(key, value, enumDefinitions)
66
+ if (enumType) typeStr = enumType
67
+ }
68
+
69
+ existingProps[key] = typeStr
70
+ bodyLines.push(` ${key}${optional}: ${typeStr};`)
71
+ })
72
+
73
+ // Optional fields from existing interface
74
+ Object.keys(existingProps).forEach(key => {
75
+ if (!(key in data)) bodyLines.push(` ${key}?: ${existingProps[key]};`)
76
+ })
77
+
78
+ const content = `export interface ${name} {\n${bodyLines.join('\n')}\n}`
79
+ allInterfaces.push(content)
80
+ }
81
+
82
+ helper(name, data)
83
+
84
+ const finalContent = [...enumDefinitions, ...allInterfaces].join('\n\n')
85
+ saveTypeFile(name, finalContent)
86
+
87
+ return finalContent
88
+ }
89
+
90
+ function parseInterface(content: string): Record<string, string> {
91
+ const props: Record<string, string> = {}
92
+ content.split('\n').forEach(line => {
93
+ const match = line.trim().match(/^(\w+)\??:\s(.+);$/)
94
+ if (match) {
95
+ const [_, key, type] = match
96
+ if (key && type) props[key] = type
97
+ }
98
+ })
99
+ return props
100
+ }
101
+
102
+ function detectEnum(key: string, value: string, enumDefinitions: string[]): string | null {
103
+ if (!allowedEnumFields.includes(key)) return null
104
+ if (typeof value !== 'string') return null
105
+
106
+ if (!fieldValuesMap[key]) fieldValuesMap[key] = new Set()
107
+ fieldValuesMap[key].add(value)
108
+
109
+ const uniqueValues = Array.from(fieldValuesMap[key])
110
+ if (uniqueValues.length > 1 && uniqueValues.length <= 10) {
111
+ const typeName = capitalize(key)
112
+ if (!generatedEnums.has(typeName)) {
113
+ generatedEnums.add(typeName)
114
+ const unionType = uniqueValues.map(v => `"${v}"`).join(' | ')
115
+ enumDefinitions.push(`export type ${typeName} = ${unionType};`)
116
+ }
117
+ return typeName
118
+ }
119
+
120
+ return null
121
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { generateInterface } from './generator/interfaceGenerator'
2
+ import { saveTypeFile } from './storage/fileWriter'
3
+
4
+ export function createTypeFromData(
5
+ name: string,
6
+ data: any,
7
+ options?: { modify?: boolean } // pass { modify: true } to update existing interface
8
+ ) {
9
+ const typeString = generateInterface(name, data, options)
10
+ const filePath = saveTypeFile(name, typeString) // saves all generated interfaces (including nested)
11
+
12
+ return {
13
+ name,
14
+ filePath,
15
+ type: typeString,
16
+ }
17
+ }
@@ -0,0 +1,45 @@
1
+ const generated = new Set<string>()
2
+
3
+ export function inferType(
4
+ value: any,
5
+ key?: string
6
+ ): string {
7
+ if (value === null) return 'null'
8
+
9
+ // ARRAY
10
+ if (Array.isArray(value)) {
11
+ if (value.length === 0) return 'any[]'
12
+
13
+ const first = value[0]
14
+
15
+ if (typeof first === 'object' && first !== null) {
16
+ const typeName = capitalize(singularize(key || 'Item'))
17
+
18
+ // Return reference to type name
19
+ // Generation will be handled in generateInterface
20
+ return `${typeName}[]`
21
+ }
22
+
23
+ return `${typeof first}[]`
24
+ }
25
+
26
+ // OBJECT
27
+ if (typeof value === 'object') {
28
+ // Inline object
29
+ const entries = Object.entries(value)
30
+ .map(([k, v]) => `${k}: ${inferType(v, k)};`)
31
+ .join(' ')
32
+ return `{ ${entries} }`
33
+ }
34
+
35
+ return typeof value
36
+ }
37
+
38
+ // Helpers
39
+ function singularize(word: string) {
40
+ return word.endsWith('s') ? word.slice(0, -1) : word
41
+ }
42
+
43
+ function capitalize(word: string) {
44
+ return word.charAt(0).toUpperCase() + word.slice(1)
45
+ }
@@ -0,0 +1,22 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ export function saveTypeFile(name: string, content: string) {
5
+ const dir = path.resolve(process.cwd(), 'types/')
6
+
7
+ if (!fs.existsSync(dir)) {
8
+ fs.mkdirSync(dir, { recursive: true })
9
+ }
10
+
11
+ const filePath = path.join(dir, `${name}.ts`)
12
+ fs.writeFileSync(filePath, content)
13
+
14
+ return filePath
15
+ }
16
+
17
+ // Add helper to read existing interface
18
+ export function readTypeFile(name: string) {
19
+ const filePath = path.join(process.cwd(), 'types', `${name}.ts`)
20
+ if (!fs.existsSync(filePath)) return null
21
+ return fs.readFileSync(filePath, 'utf-8')
22
+ }
@@ -0,0 +1,9 @@
1
+ // ------------------- Helpers -------------------
2
+
3
+ export function singularize(word: string) {
4
+ return word.endsWith('s') ? word.slice(0, -1) : word
5
+ }
6
+
7
+ export function capitalize(word: string) {
8
+ return word.charAt(0).toUpperCase() + word.slice(1)
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./dist",
5
+
6
+ "module": "NodeNext",
7
+ "moduleResolution": "NodeNext",
8
+ "target": "ES2020",
9
+
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true,
13
+
14
+ "strict": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "exactOptionalPropertyTypes": true,
17
+
18
+ "esModuleInterop": true,
19
+ "resolveJsonModule": true,
20
+
21
+ "skipLibCheck": true,
22
+ "forceConsistentCasingInFileNames": true
23
+ },
24
+ "include": ["src"],
25
+ "exclude": ["node_modules", "dist"]
26
+ }