Skip to content
2 changes: 2 additions & 0 deletions factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AnyTypeNodeParser } from "../src/NodeParser/AnyTypeNodeParser.js";
import { ArrayLiteralExpressionNodeParser } from "../src/NodeParser/ArrayLiteralExpressionNodeParser.js";
import { ArrayNodeParser } from "../src/NodeParser/ArrayNodeParser.js";
import { AsExpressionNodeParser } from "../src/NodeParser/AsExpressionNodeParser.js";
import { BinaryExpressionNodeParser } from "../src/NodeParser/BinaryExpressionNodeParser.js";
import { BooleanLiteralNodeParser } from "../src/NodeParser/BooleanLiteralNodeParser.js";
import { BooleanTypeNodeParser } from "../src/NodeParser/BooleanTypeNodeParser.js";
import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser.js";
Expand Down Expand Up @@ -114,6 +115,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
.addNodeParser(new NeverTypeNodeParser())
.addNodeParser(new ObjectTypeNodeParser())
.addNodeParser(new AsExpressionNodeParser(chainNodeParser))
.addNodeParser(new BinaryExpressionNodeParser(chainNodeParser))
.addNodeParser(new SatisfiesNodeParser(chainNodeParser))
.addNodeParser(withJsDoc(new ParameterParser(chainNodeParser)))
.addNodeParser(new StringLiteralNodeParser())
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"release": "npm run build && auto shipit",
"run": "tsx ts-json-schema-generator.ts",
"test": "jest test/ --verbose",
"test:debug": "node --inspect-brk node_modules/.bin/jest test/ --verbose --runInBand",
"test:coverage": "npm run jest -- test/ --collectCoverage=true",
"test:fast": "cross-env FAST_TEST=1 jest test/ --verbose",
"test:update": "cross-env UPDATE_SCHEMA=true npm run test:fast",
Expand Down
99 changes: 99 additions & 0 deletions src/NodeParser/BinaryExpressionNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import ts from "typescript";
import type { Context, NodeParser } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import { AnyType } from "../Type/AnyType.js";
import type { BaseType } from "../Type/BaseType.js";
import { BooleanType } from "../Type/BooleanType.js";
import { LiteralType } from "../Type/LiteralType.js";
import { NumberType } from "../Type/NumberType.js";
import { StringType } from "../Type/StringType.js";
import { UnionType } from "../Type/UnionType.js";
import { AliasType } from "../Type/AliasType.js";

export class BinaryExpressionNodeParser implements SubNodeParser {
public constructor(protected childNodeParser: NodeParser) {}

public supportsNode(node: ts.Node): boolean {
return node.kind === ts.SyntaxKind.BinaryExpression;
}

public createType(node: ts.BinaryExpression, context: Context): BaseType {
const leftType = this.childNodeParser.createType(node.left, context);
const rightType = this.childNodeParser.createType(node.right, context);

if (leftType instanceof AnyType || rightType instanceof AnyType) {
return new AnyType();
}

if (this.isStringLike(leftType) || this.isStringLike(rightType)) {
return new StringType();
}

if (this.isDefinitelyNumberLike(leftType) && this.isDefinitelyNumberLike(rightType)) {
return new NumberType();
}

if (this.isBooleanLike(leftType) && this.isBooleanLike(rightType)) {
return new BooleanType();
}

// Anything else (objects, any, unknown, weird unions, etc.) return
// 'string' because at runtime + will usually go through ToPrimitive and
// end up in the "string concatenation" branch when non-numeric stuff is
// involved.
return new StringType();
}

private isStringLike(type: BaseType): boolean {
if (type instanceof AliasType) {
return this.isStringLike(type.getType());
}

if (type instanceof StringType) {
return true;
}

if (type instanceof LiteralType && type.isString()) {
return true;
}

// Any union member being string-like is enough.
if (type instanceof UnionType) {
return type.getTypes().some((t) => this.isStringLike(t));
}

return false;
}

private isBooleanLike(type: BaseType): boolean {
if (type instanceof BooleanType) {
return true;
}

if (type instanceof LiteralType && typeof type.getValue() === "boolean") {
return true;
}

return false;
}

private isDefinitelyNumberLike(type: BaseType): boolean {
if (type instanceof AliasType) {
return this.isDefinitelyNumberLike(type.getType());
}

if (type instanceof NumberType) {
return true;
}

if (type instanceof LiteralType && typeof type.getValue() === "number") {
return true;
}

if (type instanceof UnionType) {
return type.getTypes().every((t) => this.isDefinitelyNumberLike(t));
}

return false;
}
}
1 change: 1 addition & 0 deletions test/valid-data-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe("valid-data-type", () => {
it("type-aliases-object", assertValidSchema("type-aliases-object", "MyAlias"));
it("type-aliases-mixed", assertValidSchema("type-aliases-mixed", "MyObject"));
it("type-aliases-union", assertValidSchema("type-aliases-union", "MyUnion"));
it("binary-expression", assertValidSchema("binary-expression", "MyObject"));
it("type-aliases-anonymous", assertValidSchema("type-aliases-anonymous", "MyObject"));
it("type-aliases-local-namespace", assertValidSchema("type-aliases-local-namespace", "MyObject"));
it("type-aliases-recursive-anonymous", assertValidSchema("type-aliases-recursive-anonymous", "MyAlias"));
Expand Down
66 changes: 66 additions & 0 deletions test/valid-data/binary-expression/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
type StringUnion = "a" | "b";
type NumberUnion = 10 | 20;
type MixedUnion = "c" | 30;

function getAny(): any {
return "test" as any;
}

function getBoolean(): boolean {
return Math.random() > 0.5;
}

function getStringUnion(): StringUnion {
return Math.random() > 0.5 ? "a" : "b";
}

function getNumberUnion(): NumberUnion {
return Math.random() > 0.5 ? 10 : 20;
}

function getMixedUnion(): MixedUnion {
return Math.random() > 0.5 ? "c" : 30;
}

function getUnknown(): unknown {
return "unknown value";
}

function getStringType(): string {
return Math.random() > 0.5 ? "hello" : "world";
}

const anyString: any = getAny();

const aStringUnion: StringUnion = getStringUnion();
const bStringUnion: StringUnion = getStringUnion();

const tenNumberUnion: NumberUnion = getNumberUnion();
const twentyNumberUnion: NumberUnion = getNumberUnion();

const thirtyMixedUnion: MixedUnion = getMixedUnion();

const a: boolean = getBoolean();
const b: boolean = getBoolean();

const unknownValue: unknown = getUnknown();

const foo = {
numbers: 60 * 5,
stringLiterals: "a" + "b",
stringTypes: getStringType() + getStringType(),
booleanTypes: a || b,
booleanLiterals: true || false,
any: 1 + anyString,
threeNumbers: 60 * 5 + 1,
mixedStringAndNumbers: 60 * 5 + " minutes",
bigintType: BigInt(123),

unknowns: unknownValue && unknownValue,

stringUnion: aStringUnion + bStringUnion,
numberUnion: tenNumberUnion + twentyNumberUnion,
mixedUnion: thirtyMixedUnion + " is a number",
} as const;

export type MyObject = typeof foo;
40 changes: 40 additions & 0 deletions test/valid-data/binary-expression/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"numbers": { "type": "number" },
"threeNumbers": { "type": "number" },
"stringLiterals": { "type": "string" },
"stringTypes": { "type": "string" },
"mixedStringAndNumbers": { "type": "string" },
"booleanTypes": { "type": "boolean" },
"booleanLiterals": { "type": "boolean" },
"bigintType": { "type": "number" },
"numberUnion": { "type": "number" },
"stringUnion": { "type": "string" },
"unknowns": { "type": "string" },
"mixedUnion": { "type": "string" },
"any": {}
},
"required": [
"numbers",
"stringLiterals",
"stringTypes",
"booleanTypes",
"booleanLiterals",
"any",
"threeNumbers",
"mixedStringAndNumbers",
"bigintType",
"unknowns",
"stringUnion",
"numberUnion",
"mixedUnion"
],
"type": "object"
}
}
}
Loading