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 +68 -0
- package/package.json +20 -0
- package/src/generator/interfaceGenerator.ts +121 -0
- package/src/index.ts +17 -0
- package/src/infer/inferType.ts +45 -0
- package/src/storage/fileWriter.ts +22 -0
- package/src/utils/opt-func.ts +9 -0
- package/tsconfig.json +26 -0
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
|
+
}
|
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
|
+
}
|