diff --git a/factory/parser.ts b/factory/parser.ts index 3ca76ab0b..aad16e463 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -122,7 +122,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa .addNodeParser(new TypeReferenceNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new ExpressionWithTypeArgumentsNodeParser(typeChecker, chainNodeParser)) - .addNodeParser(new IndexedAccessTypeNodeParser(chainNodeParser)) + .addNodeParser(new IndexedAccessTypeNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new MappedTypeNodeParser(chainNodeParser, mergedConfig.additionalProperties)) .addNodeParser(new ConditionalTypeNodeParser(typeChecker, chainNodeParser)) diff --git a/src/NodeParser/IndexedAccessTypeNodeParser.ts b/src/NodeParser/IndexedAccessTypeNodeParser.ts index c753e087d..056db3814 100644 --- a/src/NodeParser/IndexedAccessTypeNodeParser.ts +++ b/src/NodeParser/IndexedAccessTypeNodeParser.ts @@ -12,17 +12,49 @@ import { derefType } from "../Utils/derefType"; import { getTypeByKey } from "../Utils/typeKeys"; export class IndexedAccessTypeNodeParser implements SubNodeParser { - public constructor(protected childNodeParser: NodeParser) {} + public constructor(protected typeChecker: ts.TypeChecker, protected childNodeParser: NodeParser) {} - public supportsNode(node: ts.IndexedAccessTypeNode): boolean { + public supportsNode(node: ts.TypeNode): boolean { return node.kind === ts.SyntaxKind.IndexedAccessType; } + private createIndexedType(objectType: ts.TypeNode, context: Context, indexType: BaseType) { + if (ts.isTypeReferenceNode(objectType) && indexType instanceof LiteralType) { + const declaration = this.typeChecker.getSymbolAtLocation(objectType.typeName)?.declarations?.[0]; + + if (!declaration || !ts.isTypeAliasDeclaration(declaration) || !ts.isTypeLiteralNode(declaration.type)) { + return undefined; + } + + const member = declaration.type.members.find( + (m): m is ts.PropertySignature => + ts.isPropertySignature(m) && ts.isIdentifier(m.name) && m.name.text === indexType.getValue() + ); + + if (!member?.type) { + return undefined; + } + + return this.childNodeParser.createType(member.type, context); + } + + return undefined; + } + public createType(node: ts.IndexedAccessTypeNode, context: Context): BaseType | undefined { - const objectType = derefType(this.childNodeParser.createType(node.objectType, context)); const indexType = derefType(this.childNodeParser.createType(node.indexType, context)); + if (indexType === undefined) { + return undefined; + } + + const indexedType = this.createIndexedType(node.objectType, context, indexType); + if (indexedType) { + // indexed type was successfully retrieved + return indexedType; + } - if (objectType === undefined || indexType === undefined) { + const objectType = derefType(this.childNodeParser.createType(node.objectType, context)); + if (objectType === undefined) { return undefined; } diff --git a/src/NodeParser/TypeReferenceNodeParser.ts b/src/NodeParser/TypeReferenceNodeParser.ts index ae14c8c39..f961eb408 100644 --- a/src/NodeParser/TypeReferenceNodeParser.ts +++ b/src/NodeParser/TypeReferenceNodeParser.ts @@ -6,7 +6,7 @@ import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; import { StringType } from "../Type/StringType"; -const invlidTypes: { [index: number]: boolean } = { +const invalidTypes: { [index: number]: boolean } = { [ts.SyntaxKind.ModuleDeclaration]: true, [ts.SyntaxKind.VariableDeclaration]: true, }; @@ -23,7 +23,7 @@ export class TypeReferenceNodeParser implements SubNodeParser { if (typeSymbol.flags & ts.SymbolFlags.Alias) { const aliasedSymbol = this.typeChecker.getAliasedSymbol(typeSymbol); return this.childNodeParser.createType( - aliasedSymbol.declarations!.filter((n: ts.Declaration) => !invlidTypes[n.kind])[0], + aliasedSymbol.declarations!.filter((n: ts.Declaration) => !invalidTypes[n.kind])[0], this.createSubContext(node, context) ); } else if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) { @@ -40,7 +40,7 @@ export class TypeReferenceNodeParser implements SubNodeParser { return new AnnotatedType(new StringType(), { format: "regex" }, false); } else { return this.childNodeParser.createType( - typeSymbol.declarations!.filter((n: ts.Declaration) => !invlidTypes[n.kind])[0], + typeSymbol.declarations!.filter((n: ts.Declaration) => !invalidTypes[n.kind])[0], this.createSubContext(node, context) ); } diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index 1bc376d36..74c13b192 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -73,6 +73,7 @@ describe("valid-data-type", () => { it("type-indexed-access-object-1", assertValidSchema("type-indexed-access-object-1", "MyType")); it("type-indexed-access-object-2", assertValidSchema("type-indexed-access-object-2", "MyType")); it("type-indexed-access-keyof", assertValidSchema("type-indexed-access-keyof", "MyType")); + it("type-indexed-circular-access", assertValidSchema("type-indexed-circular-access", "*")); it("type-keyof-tuple", assertValidSchema("type-keyof-tuple", "MyType")); it("type-keyof-object", assertValidSchema("type-keyof-object", "MyType")); it("type-keyof-object-function", assertValidSchema("type-keyof-object-function", "MyType")); diff --git a/test/valid-data/type-indexed-circular-access/main.ts b/test/valid-data/type-indexed-circular-access/main.ts new file mode 100644 index 000000000..22577fb7f --- /dev/null +++ b/test/valid-data/type-indexed-circular-access/main.ts @@ -0,0 +1,12 @@ +type Alpha = { stringProp: string; betaStringProp: Beta['stringProp']; } +type TypedAlphaWithDefault = { tProp: T; betaStringProp: Beta['stringProp']; } +type TypedAlphaWithoutDefault = { tProp: T; eProp: E; betaStringProp: Beta['stringProp']; } + +export type Beta = { + stringProp: string; + alphaStringProp: Alpha['stringProp']; + alphaNumberDefaultProp: TypedAlphaWithDefault["tProp"] + alphaNumberWithoutDefaultProp: TypedAlphaWithoutDefault["tProp"] + alphaUnionProp: TypedAlphaWithoutDefault["tProp" | "eProp"] + alphaKeyofProp: TypedAlphaWithoutDefault[keyof TypedAlphaWithoutDefault] +} diff --git a/test/valid-data/type-indexed-circular-access/schema.json b/test/valid-data/type-indexed-circular-access/schema.json new file mode 100644 index 000000000..431339950 --- /dev/null +++ b/test/valid-data/type-indexed-circular-access/schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Beta", + "definitions": { + "Beta": { + "type": "object", + "properties": { + "alphaNumberDefaultProp": { + "type": "number" + }, + "alphaNumberWithoutDefaultProp": { + "type": "number" + }, + "stringProp": { + "type": "string" + }, + "alphaStringProp": { + "type": "string" + }, + "alphaUnionProp": { + "type": [ + "number", + "null", + "string" + ] + }, + "alphaKeyofProp": { + "type": [ + "number", + "null", + "string" + ] + } + }, + "required": [ + "stringProp", + "alphaStringProp", + "alphaNumberDefaultProp", + "alphaNumberWithoutDefaultProp", + "alphaUnionProp", + "alphaKeyofProp" + ], + "additionalProperties": false + } + } +}