Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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
2 changes: 2 additions & 0 deletions factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js";
import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js";
import { SpreadElementNodeParser } from "../src/NodeParser/SpreadElementNodeParser.js";
import { IdentifierNodeParser } from "../src/NodeParser/IdentifierNodeParser.js";
import { ReturnTypeNodeParser } from "../src/NodeParser/ReturnTypeNodeParser.js";

export type ParserAugmentor = (parser: MutableParser) => void;

Expand Down Expand Up @@ -130,6 +131,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
.addNodeParser(new ParenthesizedNodeParser(chainNodeParser))

.addNodeParser(new PromiseNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new ReturnTypeNodeParser(chainNodeParser, typeChecker))
.addNodeParser(new TypeReferenceNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new ExpressionWithTypeArgumentsNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new IndexedAccessTypeNodeParser(typeChecker, chainNodeParser))
Expand Down
134 changes: 134 additions & 0 deletions src/NodeParser/ReturnTypeNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import ts from "typescript";
import type { Context, NodeParser } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import type { BaseType } from "../Type/BaseType.js";
import { UnknownNodeError } from "../Error/Errors.js";
import { ObjectType } from "../Type/ObjectType.js";

Check failure on line 6 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'ObjectType' is defined but never used

Check failure on line 6 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ObjectType' is defined but never used

export class ReturnTypeNodeParser implements SubNodeParser {
constructor(
private readonly childNodeParser: NodeParser,
private readonly checker: ts.TypeChecker,
) {}

supportsNode(node: ts.Node): boolean {
if (!ts.isTypeReferenceNode(node)) {
return false;
}

Check failure on line 18 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Delete `········`

Check failure on line 18 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Delete `········`
// Check if it's a ReturnType reference
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove unnecessary comments that just explain what happens

Copy link
Author

Choose a reason for hiding this comment

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

Removed

try {
const typeName = ts.isIdentifier(node.typeName)

Check failure on line 21 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Replace `⏎················?·node.typeName.text·⏎···············` with `?·node.typeName.text`

Check failure on line 21 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Replace `⏎················?·node.typeName.text·⏎···············` with `?·node.typeName.text`
? node.typeName.text
: node.typeName.getText();
return typeName === 'ReturnType' && node.typeArguments?.length === 1;

Check failure on line 24 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Replace `'ReturnType'` with `"ReturnType"`

Check failure on line 24 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Replace `'ReturnType'` with `"ReturnType"`
} catch {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need a try catch?

Copy link
Author

Choose a reason for hiding this comment

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

Removed the try/catch.

return false;
}
}

createType(node: ts.TypeReferenceNode, context: Context): BaseType {
try {

Check failure on line 31 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Unnecessary try/catch wrapper

Check failure on line 31 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Unnecessary try/catch wrapper
if (!node.typeArguments || node.typeArguments.length !== 1) {
throw new UnknownNodeError(node);
}

const typeArg = node.typeArguments[0];

Check failure on line 37 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Delete `············`

Check failure on line 37 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Delete `············`
// Handle different types of type arguments
if (ts.isTypeQueryNode(typeArg)) {
// Case: ReturnType<typeof functionName>
// Get the symbol for the identifier
const symbol = this.checker.getSymbolAtLocation(typeArg.exprName);
if (!symbol) {
throw new UnknownNodeError(node);
}

// Get the declarations of the symbol
const declarations = symbol.getDeclarations() || [];

// Try multiple methods to extract return type
for (const decl of declarations) {
let returnTypeNode: ts.TypeNode | undefined;

// If declaration is a function/method with explicit return type
if (
(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl) ||

Check failure on line 56 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Replace `·ts.isMethodDeclaration(decl)·||·` with `⏎····························ts.isMethodDeclaration(decl)·||`

Check failure on line 56 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Replace `·ts.isMethodDeclaration(decl)·||·` with `⏎····························ts.isMethodDeclaration(decl)·||`
ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) &&

Check failure on line 57 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Replace `·························ts.isArrowFunction(decl)·||·ts.isFunctionExpression(decl))·&&·` with `····························ts.isArrowFunction(decl)·||⏎····························ts.isFunctionExpression(decl))·&&`

Check failure on line 57 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Replace `·························ts.isArrowFunction(decl)·||·ts.isFunctionExpression(decl))·&&·` with `····························ts.isArrowFunction(decl)·||⏎····························ts.isFunctionExpression(decl))·&&`
decl.type
) {
returnTypeNode = decl.type;
}

Check failure on line 61 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Delete `·`

Check failure on line 61 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Delete `·`
// If declaration is a variable with function type annotation
else if (

Check failure on line 63 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

Replace `⏎························ts.isVariableDeclaration(decl)·&&·⏎························decl.type·&&·⏎························ts.isFunctionTypeNode(decl.type)⏎····················` with `ts.isVariableDeclaration(decl)·&&·decl.type·&&·ts.isFunctionTypeNode(decl.type)`

Check failure on line 63 in src/NodeParser/ReturnTypeNodeParser.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Replace `⏎························ts.isVariableDeclaration(decl)·&&·⏎························decl.type·&&·⏎························ts.isFunctionTypeNode(decl.type)⏎····················` with `ts.isVariableDeclaration(decl)·&&·decl.type·&&·ts.isFunctionTypeNode(decl.type)`
ts.isVariableDeclaration(decl) &&
decl.type &&
ts.isFunctionTypeNode(decl.type)
) {
returnTypeNode = decl.type.type;
}

// If we found a return type node, process it
if (returnTypeNode) {
const baseType = this.childNodeParser.createType(returnTypeNode, context);
return baseType;
}
}

// Fallback to type checking method
const type = this.checker.getTypeOfSymbolAtLocation(symbol, typeArg);
Copy link
Member

Choose a reason for hiding this comment

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

Same as below. In what cases do we need the fallback? Why can't we use the function parser instead of implementing a lot of custom logic here?

Copy link
Author

Choose a reason for hiding this comment

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

Removing this fallback causes issues with implicit function return types. I added a test for this case.

Error: /home/rockerboo/code/others/ts-json-schema-generator/test-no-explicit-return.ts(5,34): error TSJ - 100: Unknown node of kind "TypeReference"

     Error: Unknown node of kind "TypeReference"
         at ReturnTypeNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/NodeParser/ReturnTypeNodeParser.ts:110:15)
         at ChainNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/ChainNodeParser.ts:37:49)
         at TypeAliasNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/NodeParser/TypeAliasNodeParser.ts:40:43)
         at AnnotatedNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/NodeParser/AnnotatedNodeParser.ts:34:47)
         at ExposeNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/ExposeNodeParser.ts:23:45)
         at CircularReferenceNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/CircularReferenceNodeParser.ts:24:43)
         at ChainNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/ChainNodeParser.ts:37:49)
         at TopRefNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/TopRefNodeParser.ts:14:47)
         at <anonymous> (/home/rockerboo/code/others/ts-json-schema-generator/src/SchemaGenerator.ts:31:39)
         at Array.map (<anonymous>)

Copy link
Member

Choose a reason for hiding this comment

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

Can we reuse some of the existing function parsing logic here?

Copy link
Author

@rockerBOO rockerBOO Jul 22, 2025

Choose a reason for hiding this comment

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

I tried to poke at it in terms of utilizing the FunctionNodeParser but since it's like an abstraction on top of functions I couldn't find a simple way of doing that. I am not fully aware of how this all works, so I do not know how to make it work well enough like this. Also we have it separated as "functions" but this is more of a data structure format (object, number, boolean, string) because of the ReturnType unless it returns a function (which might need to be further tested).

I can try to make it better but I am just not sure how to connect them.

I added more tests for the different function types though, and also tested methods.

const signatures = type.getCallSignatures();

if (signatures.length > 0) {
// Use getReturnType directly from the signature
const returnType = signatures[0].getReturnType();

const returnTypeNode = this.checker.typeToTypeNode(
returnType,
undefined,
ts.NodeBuilderFlags.NoTruncation
);

if (returnTypeNode) {
return this.childNodeParser.createType(returnTypeNode, context);
}
}
} else {
// Case: ReturnType<SomeType["methodName"]> or other complex types
// Get the type directly from TypeScript's type system
const argType = this.checker.getTypeAtLocation(typeArg);
Copy link
Member

Choose a reason for hiding this comment

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

I really want to a lid the type checker if possible. What cases fail when we remove this branch?

Copy link
Author

Choose a reason for hiding this comment

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

When removing the else branch completely:

test/valid-data/type-return-type-complex/main.ts(15,28): error TSJ - 100: Unknown node of kind "TypeReference"

     Error: Unknown node of kind "TypeReference"
         at ReturnTypeNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/NodeParser/ReturnTypeNodeParser.ts:98:19)
         at ChainNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/ChainNodeParser.ts:37:49)
         at TypeAliasNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/NodeParser/TypeAliasNodeParser.ts:40:43)
         at AnnotatedNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/NodeParser/AnnotatedNodeParser.ts:34:47)
         at ExposeNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/ExposeNodeParser.ts:23:45)
         at CircularReferenceNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/CircularReferenceNodeParser.ts:24:43)
         at ChainNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/ChainNodeParser.ts:37:49)
         at TopRefNodeParser.createType (/home/rockerboo/code/others/ts-json-schema-generator/src/TopRefNodeParser.ts:14:47)
         at <anonymous> (/home/rockerboo/code/others/ts-json-schema-generator/src/SchemaGenerator.ts:31:39)
         at Array.map (<anonymous>)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, but what line of the example causes this?

Copy link
Author

Choose a reason for hiding this comment

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

test/valid-data/type-return-type-complex/main.ts(15,28)

https://github.com/vega/ts-json-schema-generator/pull/2317/files#diff-410929871b91b13bf666ba5f7b9b865ef50c52318faaa956ef8e99a6cc996c5aR15

Specifically the node ReturnType

If you give me some more context of what to look at, I can take a deeper look to finding a better solution.


// If it's a function type, get its return type
const signatures = argType.getCallSignatures();
if (signatures.length > 0) {
const returnType = signatures[0].getReturnType();
const returnTypeNode = this.checker.typeToTypeNode(
returnType,
undefined,
ts.NodeBuilderFlags.NoTruncation
);

if (returnTypeNode) {
return this.childNodeParser.createType(returnTypeNode, context);
}
}

// Final fallback: try to get type directly
const type = this.checker.getTypeAtLocation(typeArg);
const typeNode = this.checker.typeToTypeNode(
type,
undefined,
ts.NodeBuilderFlags.NoTruncation
);

if (typeNode) {
return this.childNodeParser.createType(typeNode, context);
}
}

throw new UnknownNodeError(node);
} catch (error) {
throw error;
Copy link
Member

Choose a reason for hiding this comment

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

This seems like a noop?

Copy link
Author

Choose a reason for hiding this comment

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

Removed the try/catch

}
}
}
9 changes: 9 additions & 0 deletions test/valid-data-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,13 @@ describe("valid-data-type", () => {
"export-star-prune-unreachable",
assertValidSchema("export-star-prune-unreachable", "*", undefined, { mainTsOnly: true }),
);

it(
"type-return-type",
assertValidSchema("type-return-type", "Greeting")
);
it(
"type-return-type-complex",
assertValidSchema("type-return-type-complex", "TestAppState")
);
});
15 changes: 15 additions & 0 deletions test/valid-data/type-return-type-complex/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Simulated Redux Toolkit scenario
export interface TestState {
counter: number;
name: string;
}

export function createTestStore() {
return {
getState: () => ({ counter: 0, name: "test" } as TestState),
dispatch: (action: any) => {},
};
}

export type TestAppStore = ReturnType<typeof createTestStore>;
export type TestAppState = ReturnType<TestAppStore["getState"]>;
25 changes: 25 additions & 0 deletions test/valid-data/type-return-type-complex/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$ref": "#/definitions/TestAppState",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"TestAppState": {
"$ref": "#/definitions/TestState"
},
"TestState": {
"additionalProperties": false,
"properties": {
"counter": {
"type": "number"
},
"name": {
"type": "string"
}
},
"required": [
"counter",
"name"
],
"type": "object"
}
}
}
5 changes: 5 additions & 0 deletions test/valid-data/type-return-type/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function greet(name: string): { message: string } {
return { message: `Hello, ${name}!` };
}

export type Greeting = ReturnType<typeof greet>;
18 changes: 18 additions & 0 deletions test/valid-data/type-return-type/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$ref": "#/definitions/Greeting",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Greeting": {
"additionalProperties": false,
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
Loading