Logo

Generating TypeScript Types from JSON Data - A Custom Solution

typescript

Introduction

I wanted an easy way to generate TypeScript types from JSON data fetched from a CMS. There are a lot of libraries out there, like quicktype and json-schema-to-typescript, but they just didn’t work for me. So, I decided to create my own solution. In this article, I’ll show you the code I came up with to automatically generate TypeScript types from a JSON file.

The JSON File

1{
2 title: 'test',
3 id: 1111,
4 startDate: '2024-07-02T15:00:00.000Z',
5 endDate: '2024-07-29T17:01:00.000Z',
6 thumbnailImage: null,
7}

Approach 1 : Using Quicktype

The Code

I used the code snippet from the official documentation

The Result 🤷

1export interface Test {
2 any
3}

Approach 2: Using json-schema-to-typescript

The Code

1import { compile, compileFromFile } from 'json-schema-to-typescript'
2
3if (!fs.existsSync('./src/libs/aem/types/type.d.ts')) {
4 compile(data, 'Schema').then((ts) => {
5 fs.writeFileSync('./src/type.d.ts', ts)
6 })
7}

The Result 🤷 (Another Fail)

1export interface Test {
2 [k: string]: unknown;
3}

I'm not sure if the problem is because my JSON data has fields that aren't filled by the user (resulting in null) or if there's something wrong with my code. I got tired of trying to find the bug and just decided to write it myself instead.

Approach 3 : Writing my own code

The Code

1import * as fs from 'fs'
2import path from 'path'
3
4type JSONValue = string | number | boolean | JSONObject | JSONArray | unknown
5
6export interface JSONObject {
7 [key: string]: JSONValue
8}
9
10interface JSONArray extends Array<JSONValue> {}
11
12function generateTypeScriptType(
13 name: string,
14 json: JSONObject | JSONArray
15): string {
16 let result = ''
17
18 if (Array.isArray(json)) {
19 result += `type ${name} = ${getType(json)}[];\n`
20 } else {
21 result += `interface ${name} {\n`
22 for (const key in json) {
23 if (json[key]) {
24 result += ` ${key}: ${getType(json[key])};\n`
25 }
26 }
27 result += `}\n`
28 }
29
30 return result
31}
32
33function getType(value: JSONValue): string {
34 if (Array.isArray(value)) {
35 return value.length > 0 ? `${getType(value[0])}[]` : 'unknown[]'
36 }
37
38 if (value === null) {
39 return 'null'
40 }
41
42 switch (typeof value) {
43 case 'object':
44 return `{\n${Object.entries(value)
45 .map(([key, val]) => ` ${key}: ${getType(val)};`)
46 .join('\n')}\n}`
47 case 'string':
48 return 'string'
49 case 'number':
50 return 'number'
51 case 'boolean':
52 return 'boolean'
53 default:
54 return 'unknown'
55 }
56}
57
58function capitalizeFirstLetter(string: string) {
59 if (typeof string !== 'string' || string.length === 0) {
60 return string
61 }
62 return string.charAt(0).toUpperCase() + string.slice(1)
63}
64
65type Props = {
66 typeName: string
67 filename: string
68 data: JSONObject | JSONArray
69}
70
71export function generateTypeFromJson({ typeName, filename, data }: Props) {
72 const typeDefinitions = generateTypeScriptType(
73 capitalizeFirstLetter(typeName),
74 data
75 )
76 const filePath = path.resolve(
77 'src/libs/aem/generateType/',
78 `${filename.toLowerCase()}.d.ts`
79 )
80
81 if (!fs.existsSync(filePath)) {
82 fs.writeFileSync(filePath, typeDefinitions)
83 } else {
84 const currentContent = fs.readFileSync(filePath, 'utf8')
85 if (currentContent !== typeDefinitions) {
86 fs.writeFileSync(filePath, typeDefinitions)
87 }
88 }
89}

Usage

1const data = await functionThatFetchesData()
2
3if (data) {
4 generateTypeFromJson({
5 typeName: 'Test',
6 filename: 'test',
7 data: data,
8 })
9}

The Result 🎉

1interface Test {
2 title: string;
3 id: number;
4 startDate: string;
5 endDate: string;
6 thumbnailImage: null,
7}