Skip to content

Commit 7d16337

Browse files
author
k.golikov
committed
JSON-To-TypeScript: merging types
1 parent fd573ee commit 7d16337

9 files changed

+194
-62
lines changed

src/pages/jsonToTypeScriptPage/JsonToTypeScriptPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ const JsonToTypeScriptPage = () => {
3131
}
3232

3333
const typeScriptType = getTypeScriptType('Root', parseJsonObject(JSON.parse(json)));
34-
return getAllTypeScriptTypeDeclarations(typeScriptType, 'Root', ExportType.ES_MODULE);
34+
return getAllTypeScriptTypeDeclarations(typeScriptType, 'Root', {
35+
exportType: ExportType.ES_MODULE,
36+
isReversedOrder: true
37+
});
3538
},
3639
[json],
3740
50
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import ExportType from './ExportType';
2+
3+
interface TypeScriptDeclarationOptions {
4+
exportType?: ExportType;
5+
isReversedOrder?: boolean;
6+
}
7+
8+
export default TypeScriptDeclarationOptions;

src/pages/jsonToTypeScriptPage/types/typescript.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { JsonPrimitive } from './json';
22
import getTypeScriptTypeReference from '../utils/getTypeScriptTypeReference';
33
import mapObject from '../../../utils/mapObject';
44
import ExportType from './ExportType';
5+
import { isObject, isString } from 'lodash';
6+
import TypeScriptDeclarationOptions from './TypeScriptDeclarationOptions';
57

68
export interface ITypeScriptType {
79
stringifyReference(): string;
@@ -12,8 +14,38 @@ export interface IDeclarable {
1214
}
1315

1416
export interface IDeclarableTypeScriptType extends IDeclarable {
15-
readonly name: string;
16-
stringifyDeclaration(exportType?: ExportType): string;
17+
name: string;
18+
stringifyDeclaration(options: TypeScriptDeclarationOptions): string;
19+
}
20+
21+
export class DeclarableTypeScriptType implements IDeclarableTypeScriptType {
22+
constructor(name: string, type: IDeclarableTypeScriptType | TypeScriptType) {
23+
this.name = name;
24+
this.type = type;
25+
}
26+
27+
public readonly name: string;
28+
private readonly type: IDeclarableTypeScriptType | TypeScriptType;
29+
30+
stringifyDeclaration(options: TypeScriptDeclarationOptions): string {
31+
if (isObject(this.type) && 'stringifyDeclaration' in this.type) {
32+
return this.type.stringifyDeclaration(options);
33+
}
34+
35+
return `${getExportKeyword(options.exportType)}type ${this.name} = ${this.stringifyDeclarationBody()}`;
36+
}
37+
38+
stringifyDeclarationBody(): string {
39+
if (isString(this.type)) {
40+
return this.type;
41+
}
42+
43+
return 'stringifyDeclarationBody' in this.type
44+
? this.type.stringifyDeclarationBody()
45+
: 'stringifyReference' in this.type
46+
? this.type.stringifyReference()
47+
: '';
48+
}
1749
}
1850

1951
export class TypeScriptUnknown implements ITypeScriptType {
@@ -33,9 +65,9 @@ export class TypeScriptObjectField implements IDeclarable {
3365
}
3466

3567
export class TypeScriptInterface implements ITypeScriptType, IDeclarableTypeScriptType {
36-
public constructor(public readonly name: string, public readonly fields: Record<string, TypeScriptObjectField>) {}
68+
public constructor(public name: string, public readonly fields: Record<string, TypeScriptObjectField>) {}
3769

38-
stringifyDeclaration(exportType?: ExportType): string {
70+
stringifyDeclaration({ exportType }: TypeScriptDeclarationOptions): string {
3971
return `${getExportKeyword(exportType)}interface ${this.name} ${this.stringifyDeclarationBody()}`;
4072
}
4173

@@ -70,7 +102,7 @@ export class TypeScriptArray implements ITypeScriptType {
70102

71103
export class TypeScriptUnion implements ITypeScriptType {
72104
//, IDeclarableTypeScriptType {
73-
public constructor(public readonly name: string, public readonly types: TypeScriptType[]) {}
105+
public constructor(public name: string, public readonly types: TypeScriptType[]) {}
74106

75107
// stringifyDeclaration(exportType?: ExportType): string {
76108
// return `${getExportKeyword(exportType)}type ${this.name} = ${this.stringifyDeclarationBody()};`;
@@ -87,7 +119,7 @@ export class TypeScriptUnion implements ITypeScriptType {
87119

88120
//TODO add tuples support
89121
export class TypeScriptTuple implements ITypeScriptType {
90-
public constructor(public readonly name: string, public readonly types: TypeScriptType[]) {}
122+
public constructor(public name: string, public readonly types: TypeScriptType[]) {}
91123

92124
stringifyDeclarationBody(): string {
93125
return '[' + this.types.map(getTypeScriptTypeReference).join(', ') + ']';

src/pages/jsonToTypeScriptPage/utils/getAllTypeScriptTypeDeclarableTypes.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
1-
import getAllTypeScriptTypeDeclarableTypes from './getAllTypeScriptTypeDeclarableTypes';
2-
import { TypeScriptType } from '../types/typescript';
3-
import getTypeScriptTypeDeclaration from './getTypeScriptTypeDeclaration';
4-
import ExportType from '../types/ExportType';
1+
import { getAllTypeScriptTypeDeclarableTypes } from './getAllTypeScriptTypeInnerDeclarableTypes';
2+
import { IDeclarableTypeScriptType, TypeScriptType } from '../types/typescript';
3+
import TypeScriptDeclarationOptions from '../types/TypeScriptDeclarationOptions';
54

6-
const getAllTypeScriptTypeDeclarations = (type: TypeScriptType, name: string, exportType?: ExportType) => {
7-
const declarableTypes = getAllTypeScriptTypeDeclarableTypes(type);
5+
const renameConflictingType = (
6+
type: IDeclarableTypeScriptType,
7+
isConflicting: (type: IDeclarableTypeScriptType) => boolean
8+
): void => {
9+
const match = /^(.*?)(\d+)$/.exec(type.name);
10+
if (!match) {
11+
type.name += '1';
12+
} else {
13+
const [, left, numericPart] = match;
14+
const numeric = Number(numericPart);
15+
type.name = `${left}${numeric + 1}`;
16+
}
817

9-
if (declarableTypes.length === 0) {
10-
return getTypeScriptTypeDeclaration(type, name, exportType);
18+
if (isConflicting(type)) {
19+
renameConflictingType(type, isConflicting);
1120
}
21+
};
22+
23+
const getAllTypeScriptTypeDeclarations = (
24+
type: TypeScriptType,
25+
name: string,
26+
options: TypeScriptDeclarationOptions
27+
): string => {
28+
const declarableTypes = getAllTypeScriptTypeDeclarableTypes(type, name, options);
29+
30+
const correctlyNamedDeclarableTypes: IDeclarableTypeScriptType[] = [];
31+
32+
declarableTypes.forEach((declarableType) => {
33+
const isConflicting = (type: IDeclarableTypeScriptType) =>
34+
correctlyNamedDeclarableTypes.some((value) => value.name === type.name);
35+
36+
if (isConflicting(declarableType)) {
37+
renameConflictingType(declarableType, isConflicting);
38+
}
39+
40+
correctlyNamedDeclarableTypes.push(declarableType);
41+
});
1242

13-
return declarableTypes
14-
.map((declarable) => getTypeScriptTypeDeclaration(declarable as TypeScriptType, name, exportType))
15-
.join('\n\n');
43+
return declarableTypes.map((declarable) => declarable.stringifyDeclaration(options)).join('\n\n');
1644
};
1745

1846
export default getAllTypeScriptTypeDeclarations;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
DeclarableTypeScriptType,
3+
IDeclarableTypeScriptType,
4+
TypeScriptArray,
5+
TypeScriptType,
6+
TypeScriptUnion,
7+
TypeScriptUnknown
8+
} from '../types/typescript';
9+
import { isObject, isString } from 'lodash';
10+
import mapObject from '../../../utils/mapObject';
11+
import TypeScriptDeclarationOptions from '../types/TypeScriptDeclarationOptions';
12+
13+
const getAllTypeScriptTypeInnerDeclarableTypes = (
14+
type: TypeScriptType,
15+
options: TypeScriptDeclarationOptions,
16+
includeSelf = false
17+
): IDeclarableTypeScriptType[] => {
18+
if (isString(type) || type instanceof TypeScriptUnknown) {
19+
return []; //getTypeScriptTypeDeclaration(type, name, exportType)
20+
}
21+
22+
if (type instanceof TypeScriptUnion) {
23+
return type.types.flatMap((innerType) => getAllTypeScriptTypeInnerDeclarableTypes(innerType, options, true));
24+
}
25+
26+
if (type instanceof TypeScriptArray) {
27+
return getAllTypeScriptTypeInnerDeclarableTypes(type.type, options, true);
28+
}
29+
30+
const result = mapObject(type.fields, (fieldName, field) =>
31+
getAllTypeScriptTypeInnerDeclarableTypes(field.type, options, true)
32+
).flatMap((value) => value);
33+
34+
if (includeSelf) {
35+
if (options.isReversedOrder) {
36+
result.unshift(type);
37+
} else {
38+
result.push(type);
39+
}
40+
}
41+
42+
return result;
43+
};
44+
45+
export const getAllTypeScriptTypeDeclarableTypes = (
46+
type: TypeScriptType,
47+
name: string,
48+
options: TypeScriptDeclarationOptions
49+
): IDeclarableTypeScriptType[] => {
50+
const declarableTypes: IDeclarableTypeScriptType[] = getAllTypeScriptTypeInnerDeclarableTypes(type, options);
51+
52+
const declarableSelf: IDeclarableTypeScriptType =
53+
isObject(type) && 'stringifyDeclaration' in type ? type : new DeclarableTypeScriptType(name, type);
54+
55+
if (options.isReversedOrder) {
56+
declarableTypes.unshift(declarableSelf);
57+
} else {
58+
declarableTypes.push(declarableSelf);
59+
}
60+
61+
return declarableTypes;
62+
};
63+
64+
export default getAllTypeScriptTypeInnerDeclarableTypes;
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import { TypeScriptType } from '../types/typescript';
1+
import { DeclarableTypeScriptType, TypeScriptType } from '../types/typescript';
22
import { isObject } from 'lodash';
3-
import ExportType from '../types/ExportType';
4-
import getTypeScriptTypeReference from './getTypeScriptTypeReference';
3+
import TypeScriptDeclarationOptions from '../types/TypeScriptDeclarationOptions';
54

6-
const getTypeScriptTypeDeclaration = (type: TypeScriptType, name: string, exportType?: ExportType): string => {
5+
const getTypeScriptTypeDeclaration = (
6+
type: TypeScriptType,
7+
name: string,
8+
options: TypeScriptDeclarationOptions
9+
): string => {
710
if (isObject(type) && 'stringifyDeclaration' in type) {
8-
return type.stringifyDeclaration(exportType);
11+
return type.stringifyDeclaration(options);
912
}
1013

11-
return `type ${name} = ${getTypeScriptTypeReference(type)};`;
14+
return new DeclarableTypeScriptType(name, type).stringifyDeclaration(options);
15+
//
16+
// return `type ${name} = ${getTypeScriptTypeReference(type)};`;
1217
};
1318

1419
export default getTypeScriptTypeDeclaration;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TypeScriptType, TypeScriptUnion, TypeScriptUnknown } from '../types/typescript';
2+
3+
const getTypeScriptUnion = (name: string, types: TypeScriptType[]): TypeScriptType => {
4+
if (types.length === 0) {
5+
return new TypeScriptUnknown();
6+
}
7+
if (types.length === 1) {
8+
return types[0];
9+
}
10+
11+
return new TypeScriptUnion(name, types);
12+
};
13+
14+
export default getTypeScriptUnion;

src/pages/jsonToTypeScriptPage/utils/mergeTypeScriptTypes.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { chain, isString } from 'lodash';
22
import { TypeScriptInterface, TypeScriptObjectField, TypeScriptType, TypeScriptUnion } from '../types/typescript';
3+
import mergeTypeScriptTypesList from './mergeTypeScriptTypesList';
34

45
const mergeTypeScriptTypes = (a: TypeScriptType, b: TypeScriptType): TypeScriptType[] => {
56
const singleType = [a];
67
const bothTypes = [a, b];
78

8-
if (a.constructor !== b.constructor) {
9-
return bothTypes;
10-
}
11-
129
if (isString(a) && isString(b)) {
1310
return a === b ? singleType : bothTypes;
1411
}
@@ -44,22 +41,35 @@ const mergeTypeScriptTypes = (a: TypeScriptType, b: TypeScriptType): TypeScriptT
4441

4542
//has both keys
4643

44+
const isOptional = aField.isOptional || bField.isOptional;
45+
4746
const mergedFieldTypes = mergeTypeScriptTypes(aField.type, bField.type);
4847
if (mergedFieldTypes.length === 0) {
4948
return result;
5049
}
5150
if (mergedFieldTypes.length === 1) {
52-
result[fieldKey] = new TypeScriptObjectField(mergedFieldTypes[0], false);
51+
result[fieldKey] = new TypeScriptObjectField(mergedFieldTypes[0], isOptional);
52+
return result;
5353
}
5454

55-
result[fieldKey] = new TypeScriptObjectField(new TypeScriptUnion(fieldKey, mergedFieldTypes));
55+
result[fieldKey] = new TypeScriptObjectField(
56+
new TypeScriptUnion(fieldKey, mergedFieldTypes),
57+
isOptional
58+
);
5659
return result;
5760
}, {} as Record<string, TypeScriptObjectField>)
5861
.value();
5962

6063
return [new TypeScriptInterface(a.name, mergedFields)];
6164
}
6265

66+
if (a instanceof TypeScriptUnion || b instanceof TypeScriptUnion) {
67+
const aTypes = a instanceof TypeScriptUnion ? a.types : [a];
68+
const bTypes = b instanceof TypeScriptUnion ? b.types : [b];
69+
70+
return mergeTypeScriptTypesList([...aTypes, ...bTypes]);
71+
}
72+
6373
return bothTypes; //TODO
6474
};
6575

0 commit comments

Comments
 (0)