Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function generate(ast: AST, options = DEFAULT_OPTIONS): string {
return (
[
options.bannerComment,
declareNamedTypes(ast, options, ast.standaloneName!),
declareNamedTypes(ast, options, ast.standaloneName!, new Set(), new Set()),
declareNamedInterfaces(ast, options, ast.standaloneName!),
declareEnums(ast, options),
]
Expand Down Expand Up @@ -101,18 +101,33 @@ function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string,
return type
}

function declareNamedTypes(ast: AST, options: Options, rootASTName: string, processed = new Set<AST>()): string {
function declareNamedTypes(
ast: AST,
options: Options,
rootASTName: string,
processed = new Set<AST>(),
outputtedNames = new Set<string>(),
): string {
if (processed.has(ast)) {
return ''
}

processed.add(ast)

// Helper to generate standalone type only if name hasn't been used
const generateStandaloneTypeOnce = (ast: ASTWithStandaloneName): string | undefined => {
if (outputtedNames.has(ast.standaloneName)) {
return undefined
}
outputtedNames.add(ast.standaloneName)
return generateStandaloneType(ast, options)
}

switch (ast.type) {
case 'ARRAY':
return [
declareNamedTypes(ast.params, options, rootASTName, processed),
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined,
declareNamedTypes(ast.params, options, rootASTName, processed, outputtedNames),
hasStandaloneName(ast) ? generateStandaloneTypeOnce(ast) : undefined,
]
.filter(Boolean)
.join('\n')
Expand All @@ -123,28 +138,28 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
.map(
ast =>
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
declareNamedTypes(ast, options, rootASTName, processed),
declareNamedTypes(ast, options, rootASTName, processed, outputtedNames),
)
.filter(Boolean)
.join('\n')
case 'INTERSECTION':
case 'TUPLE':
case 'UNION':
return [
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined,
hasStandaloneName(ast) ? generateStandaloneTypeOnce(ast) : undefined,
ast.params
.map(ast => declareNamedTypes(ast, options, rootASTName, processed))
.map(ast => declareNamedTypes(ast, options, rootASTName, processed, outputtedNames))
.filter(Boolean)
.join('\n'),
'spreadParam' in ast && ast.spreadParam
? declareNamedTypes(ast.spreadParam, options, rootASTName, processed)
? declareNamedTypes(ast.spreadParam, options, rootASTName, processed, outputtedNames)
: undefined,
]
.filter(Boolean)
.join('\n')
default:
if (hasStandaloneName(ast)) {
return generateStandaloneType(ast, options)
return generateStandaloneTypeOnce(ast) || ''
}
return ''
}
Expand Down
82 changes: 79 additions & 3 deletions src/optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,85 @@ export function optimize(ast: AST, options: Options, processed = new Set<AST>())
optimizedAST.params = params
}

return Object.assign(optimizedAST, {
params: optimizedAST.params.map(_ => optimize(_, options, processed)),
})
// For INTERSECTION: simplify A & (A | null) -> A | null
// Also handle cases where intersection contains duplicate types
if (ast.type === 'INTERSECTION') {
// First, handle the A & (A | null) -> A | null pattern
if (optimizedAST.params.length === 2) {
const [first, second] = optimizedAST.params

// Check if one is INTERFACE and the other is UNION containing that INTERFACE
if (first.type === 'INTERFACE' && second.type === 'UNION') {
const firstType = generateType(first, options)
// Look for an interface member in the union that matches first
for (const unionParam of second.params) {
if (unionParam.type === 'INTERFACE') {
const unionParamType = generateType(unionParam, options)
if (firstType === unionParamType) {
// Found a match - check if the rest of the union is just adding null or similar
const otherMembers = second.params.filter(_ => _ !== unionParam)
if (otherMembers.length > 0) {
log('cyan', 'optimizer', 'A & (A | ...) -> A | ...', optimizedAST)
// Preserve standaloneName, keyName, comment from the intersection
return {
...second,
standaloneName: optimizedAST.standaloneName,
keyName: optimizedAST.keyName,
comment: optimizedAST.comment,
deprecated: optimizedAST.deprecated,
}
}
}
}
}
}

// Check the reverse: (A | ...) & A -> A | ...
if (second.type === 'INTERFACE' && first.type === 'UNION') {
const secondType = generateType(second, options)
// Look for an interface member in the union that matches second
for (const unionParam of first.params) {
if (unionParam.type === 'INTERFACE') {
const unionParamType = generateType(unionParam, options)
if (secondType === unionParamType) {
// Found a match - check if the rest of the union is just adding null or similar
const otherMembers = first.params.filter(_ => _ !== unionParam)
if (otherMembers.length > 0) {
log('cyan', 'optimizer', '(A | ...) & A -> A | ...', optimizedAST)
// Preserve standaloneName, keyName, comment from the intersection
return {
...first,
standaloneName: optimizedAST.standaloneName,
keyName: optimizedAST.keyName,
comment: optimizedAST.comment,
deprecated: optimizedAST.deprecated,
}
}
}
}
}
}
}

// Second, handle intersections with more than 2 members that contain duplicates
// This handles cases like A & B & A & B -> A & B
if (optimizedAST.params.length > 2) {
const uniqueParams = uniqBy(optimizedAST.params, _ => generateType(_, options))
if (uniqueParams.length < optimizedAST.params.length) {
log('cyan', 'optimizer', 'Intersection with duplicates simplified', optimizedAST)
optimizedAST.params = uniqueParams

// If we're left with just 1 param, return it directly
if (uniqueParams.length === 1) {
log('cyan', 'optimizer', 'Single-member intersection unwrapped', optimizedAST)
return uniqueParams[0]
}
}
}
}

// Params were already optimized at line 27, so just return
return optimizedAST
default:
return ast
}
Expand Down
146 changes: 48 additions & 98 deletions test/__snapshots__/test/test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -440405,17 +440405,7 @@ Generated by [AVA](https://avajs.dev).
*/␊
exit_code?: number␊
[k: string]: unknown␊
} & ({␊
/**␊
* output of the postdeploy script␊
*/␊
output?: string␊
/**␊
* The exit code of the postdeploy script␊
*/␊
exit_code?: number␊
[k: string]: unknown␊
} | null))␊
} | null)␊
/**␊
* buildpacks executed for this build, in order␊
*/␊
Expand All @@ -440429,19 +440419,13 @@ Generated by [AVA](https://avajs.dev).
/**␊
* release resulting from the build␊
*/␊
export type Release = ({␊
export type Release = (null | {␊
/**␊
* unique identifier of release␊
*/␊
id?: string␊
[k: string]: unknown␊
} & (null | {␊
/**␊
* unique identifier of release␊
*/␊
id?: string␊
[k: string]: unknown␊
}))␊
})␊
/**␊
* price information for this dyno size␊
*/␊
Expand All @@ -440465,17 +440449,7 @@ Generated by [AVA](https://avajs.dev).
*/␊
name?: string␊
[k: string]: unknown␊
} & ({␊
/**␊
* unique identifier of add-on␊
*/␊
id?: string␊
/**␊
* globally unique name of the add-on␊
*/␊
name?: string␊
[k: string]: unknown␊
} | null))␊
} | null)␊
/**␊
* The scope of access OAuth authorization allows␊
*/␊
Expand Down Expand Up @@ -444001,66 +443975,7 @@ Generated by [AVA](https://avajs.dev).
* and run json-schema-to-typescript to regenerate this file.␊
*/␊
export type CoreSchemaMetaSchema = {␊
$id?: string;␊
$schema?: string;␊
$ref?: string;␊
$comment?: string;␊
title?: string;␊
description?: string;␊
default?: unknown;␊
readOnly?: boolean;␊
writeOnly?: boolean;␊
examples?: unknown[];␊
multipleOf?: number;␊
maximum?: number;␊
exclusiveMaximum?: number;␊
minimum?: number;␊
exclusiveMinimum?: number;␊
maxLength?: NonNegativeInteger;␊
minLength?: NonNegativeIntegerDefault0;␊
pattern?: string;␊
additionalItems?: CoreSchemaMetaSchema;␊
items?: CoreSchemaMetaSchema | SchemaArray;␊
maxItems?: NonNegativeInteger;␊
minItems?: NonNegativeIntegerDefault0;␊
uniqueItems?: boolean;␊
contains?: CoreSchemaMetaSchema;␊
maxProperties?: NonNegativeInteger;␊
minProperties?: NonNegativeIntegerDefault0;␊
required?: StringArray;␊
additionalProperties?: CoreSchemaMetaSchema;␊
definitions?: {␊
[k: string]: CoreSchemaMetaSchema;␊
};␊
properties?: {␊
[k: string]: CoreSchemaMetaSchema;␊
};␊
patternProperties?: {␊
[k: string]: CoreSchemaMetaSchema;␊
};␊
dependencies?: {␊
[k: string]: CoreSchemaMetaSchema | StringArray;␊
};␊
propertyNames?: CoreSchemaMetaSchema;␊
const?: unknown;␊
/**␊
* @minItems 1␊
*/␊
enum?: [unknown, ...unknown[]];␊
type?: SimpleTypes | [SimpleTypes, ...SimpleTypes[]];␊
format?: string;␊
contentMediaType?: string;␊
contentEncoding?: string;␊
if?: CoreSchemaMetaSchema;␊
then?: CoreSchemaMetaSchema;␊
else?: CoreSchemaMetaSchema;␊
allOf?: SchemaArray;␊
anyOf?: SchemaArray;␊
oneOf?: SchemaArray;␊
not?: CoreSchemaMetaSchema;␊
[k: string]: unknown;␊
} & (␊
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it was able to de-duplicate this too.

export type CoreSchemaMetaSchema =␊
| {␊
$id?: string;␊
$schema?: string;␊
Expand Down Expand Up @@ -444121,8 +444036,7 @@ Generated by [AVA](https://avajs.dev).
not?: CoreSchemaMetaSchema;␊
[k: string]: unknown;␊
}␊
| boolean␊
);␊
| boolean;␊
export type NonNegativeInteger = number;␊
export type NonNegativeIntegerDefault0 = NonNegativeInteger;␊
/**␊
Expand Down Expand Up @@ -447933,12 +447847,14 @@ Generated by [AVA](https://avajs.dev).
/**␊
* A person who has been involved in creating or maintaining this package.␊
*/␊
export type Person = {␊
name: string;␊
url?: string;␊
email?: string;␊
[k: string]: unknown;␊
} & Person1;␊
export type Person =␊
| {␊
name: string;␊
url?: string;␊
email?: string;␊
[k: string]: unknown;␊
}␊
| string;␊
export type Person1 =␊
| {␊
name: string;␊
Expand Down Expand Up @@ -448084,6 +448000,40 @@ Generated by [AVA](https://avajs.dev).
}␊
`

## realWorld.tsconfig.js

> Expected output to match snapshot for e2e test: realWorld.tsconfig.js

`/* eslint-disable */␊
/**␊
* This file was automatically generated by json-schema-to-typescript.␊
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊
* and run json-schema-to-typescript to regenerate this file.␊
*/␊
export type RealWorld = CompilerOptionsDefinition & TsNodeDefinition;␊
export type CompilerOptions = {␊
types?: (string | null)[] | null;␊
target?: string | null;␊
[k: string]: unknown;␊
} | null;␊
export interface CompilerOptionsDefinition {␊
compilerOptions?: CompilerOptions;␊
[k: string]: unknown;␊
}␊
export interface TsNodeDefinition {␊
"ts-node"?: {␊
compilerOptions?: CompilerOptions &␊
({␊
[k: string]: unknown;␊
} | null);␊
[k: string]: unknown;␊
} | null;␊
[k: string]: unknown;␊
}␊
`

## ref.1a.js

> Expected output to match snapshot for e2e test: ref.1a.js
Expand Down
Binary file modified test/__snapshots__/test/test.ts.snap
Binary file not shown.
Loading