Skip to content

Conversation

@srsudar
Copy link
Contributor

@srsudar srsudar commented Nov 25, 2025

Overview

We have a type that uses default: 60 * 5. This breaks the current parser with an error like the one shown below.

I think that the fully correct way to handle this would be to evaluate the expression and set it to something like { const: 300, type: number}.

This implementation instead ignores the const and widens the type to just number. This seems fine for my purposes--curious if this works for the maintainers.

This is the error thrown by the test added in this PR before the fix:

  ● valid-data-type › binary-expression

    Unknown node of kind "BinaryExpression"

      58 |         }
      59 |
    > 60 |         throw new UnknownNodeError(node);
         |               ^
      61 |     }
      62 | }
      63 |

      at ChainNodeParser.getNodeParser (src/ChainNodeParser.ts:60:15)
      at ChainNodeParser.getNodeParser [as createType] (src/ChainNodeParser.ts:37:29)
      at createType (src/NodeParser/ObjectLiteralExpressionNodeParser.ts:90:38)
          at Array.flatMap (<anonymous>)
      at ObjectLiteralExpressionNodeParser.flatMap [as parseProperties] (src/NodeParser/ObjectLiteralExpressionNodeParser.ts:60:27)
      at ObjectLiteralExpressionNodeParser.parseProperties [as createType] (src/NodeParser/ObjectLiteralExpressionNodeParser.ts:33:39)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at AsExpressionNodeParser.createType (src/NodeParser/AsExpressionNodeParser.ts:15:37)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at SatisfiesNodeParser.createType (src/NodeParser/SatisfiesNodeParser.ts:13:37)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at TypeofNodeParser.createType (src/NodeParser/TypeofNodeParser.ts:54:45)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at TypeAliasNodeParser.createType (src/NodeParser/TypeAliasNodeParser.ts:40:43)
      at AnnotatedNodeParser.createType (src/NodeParser/AnnotatedNodeParser.ts:34:47)
      at ExposeNodeParser.createType (src/ExposeNodeParser.ts:23:45)
      at CircularReferenceNodeParser.createType (src/CircularReferenceNodeParser.ts:24:43)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at TopRefNodeParser.createType (src/TopRefNodeParser.ts:14:47)
      at createType (src/SchemaGenerator.ts:31:39)
          at Array.map (<anonymous>)
      at SchemaGenerator.map [as createSchemaFromNodes] (src/SchemaGenerator.ts:29:33)
      at SchemaGenerator.createSchemaFromNodes [as createSchema] (src/SchemaGenerator.ts:25:21)
      at Object.createSchema (test/utils.ts:63:34)

Version

Published prerelease version: v2.5.0-next.10

Changelog

🎉 This release contains work from new contributors! 🎉

Thanks for all your work!

❤️ Sam Sudar (@srsudar)

❤️ Orta Therox (@orta)

❤️ James Vaughan (@jamesbvaughan)

❤️ Alex (@alexchexes)

❤️ Cal (@CalLavicka)

❤️ Valentyne Stigloher (@pixunil)

🚀 Enhancement

🐛 Bug Fix

🔩 Dependency Updates

Authors: 9

return node.kind === ts.SyntaxKind.BinaryExpression;
}
public createType(node: ts.BinaryExpression, context: Context): BaseType {
// For the purposes of types, assume that binary expressions always
Copy link
Collaborator

Choose a reason for hiding this comment

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

do they always evaluate to number? is 0n a binary exp too?

Copy link
Member

Choose a reason for hiding this comment

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

"a" + "b" is also a binary expression. I don't think this works.

Copy link
Collaborator

@arthurfiorette arthurfiorette Nov 25, 2025

Choose a reason for hiding this comment

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

some pseudocode I got with help from chatgpt:

  impl(node: ts.BinaryExpression): 'string' | 'number' {
    // You can assert this before calling, or guard here:
    if (node.operatorToken.kind !== ts.SyntaxKind.PlusToken) {
      throw new Error('createType only supports + binary expressions');
    }

    const leftType = this.checker.getTypeAtLocation(node.left);
    const rightType = this.checker.getTypeAtLocation(node.right);

    // 1) If either side is string-like → 'string'
    if (this.isStringLike(leftType) || this.isStringLike(rightType)) {
      return 'string';
    }

    // 2) If both sides are definitely number-like → 'number'
    if (this.isDefinitelyNumberLike(leftType) && this.isDefinitelyNumberLike(rightType)) {
      return 'number';
    }

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

  private isStringLike(type: ts.Type): boolean {
    // Use apparent type to collapse things like literal unions, etc.
    type = this.checker.getApparentType(type);

    // Union? Any member being string-like is enough.
    if (type.isUnion()) {
      return type.types.some(t => this.isStringLike(t));
    }

    const f = type.flags;

    // String primitives + string literals + template literals
    if (f & ts.TypeFlags.StringLike) {
      return true;
    }

    // Optionally treat String object type as string-like:
    const symbol = type.getSymbol();
    if (symbol && symbol.getName() === 'String') {
      return true;
    }

    return false;
  }

  private isDefinitelyNumberLike(type: ts.Type): boolean {
    // Again, use apparent type for unions/intersections
    type = this.checker.getApparentType(type);

    if (type.isUnion()) {
      // Must be number-like for *all* members to be "definitely number-like"
      return type.types.every(t => this.isDefinitelyNumberLike(t));
    }

    const f = type.flags;

    // Number, number literal, enums, bigint etc.
    // If you don't want bigint, drop BigIntLike.
    const numericFlags =
      ts.TypeFlags.NumberLike |
      ts.TypeFlags.BigIntLike |
      ts.TypeFlags.EnumLike;

    if (f & numericFlags) {
      return true;
    }

    return false;
  }

i hope it helps

Copy link
Collaborator

Choose a reason for hiding this comment

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

if (isAnyLike(left) || isAnyLike(right)) {
  return any;
}

if (isStringLike(left) || isStringLike(right)) {
  // includes unions containing string, template literal types, etc.
  return string;
}

if (isNumberLike(left) && isNumberLike(right)) {
  return number;
}

if (isBigIntLike(left) && isBigIntLike(right)) {
  return bigint;
}

// mixtures: (string | number) + (string | number) → string | number, etc.
// if nothing fits cleanly, often falls back to any / error / union

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Took another stab at this and added some more test cases.

return node.kind === ts.SyntaxKind.BinaryExpression;
}
public createType(node: ts.BinaryExpression, context: Context): BaseType {
// For the purposes of types, assume that binary expressions always
Copy link
Member

Choose a reason for hiding this comment

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

"a" + "b" is also a binary expression. I don't think this works.

@srsudar
Copy link
Contributor Author

srsudar commented Nov 25, 2025

See below is what Google's AI mode tells me about possible evaluations, which broadly makes sense to me.

The pseudcode you included was a good starting point, but I couldn't get it to quite work. The getApparentType() eg seems to return something that I can check just with the type string as Number. None of the flags set on that object were very helpful, to my surprise.

I've left in a couple things that were useful in debugging (an npm run test:debug command and a getTypeFlagNames() fn). Let me know if you'd rather me delete those.

Google's Take:

In TypeScript, a binary expression, which involves an operator acting on two operands, can evaluate to various types depending on the operator and the types of the operands.
Here are some common examples of what binary expressions can evaluate to:

• Number: Arithmetic operators like +, -, *, /, % when applied to two number operands will result in a number.

let result: number = 5 + 3; // result is 8 (number)

• String: The + operator, when one or both operands are string types, performs string concatenation and results in a string.

let message: string = "Hello " + "World"; // message is "Hello World" (string)

• Boolean: Comparison operators (==, ===, !=, !==, <, >, <=, >=) and logical operators (&&, ||) always evaluate to a boolean value.

let isEqual: boolean = (10 === 10); // isEqual is true (boolean)
let isTrue: boolean = (true && false); // isTrue is false (boolean)

• Any type: If one or both operands in a binary expression are of type any, the result of the expression will also typically be any.

let value: any = "abc" + 123; // value is "abc123" (any)

• Enum type: When enum members are used in arithmetic operations, they are treated as their underlying number values, and the result will be a number.

enum Colors { Red = 1, Green = 2 }
let sum: number = Colors.Red + Colors.Green; // sum is 3 (number)

• Union type: In conditional expressions (ternary operator ? :), the type of the expression will be a union of the types of the two possible results.

let age: number = 25;
let status: string | number = age > 18 ? "Adult" : 10; // status is "Adult" (string) or 10 (number)

AI responses may include mistakes.

@domoritz
Copy link
Member

Please fix the tests. I'll let @arthurfiorette merge.

@srsudar
Copy link
Contributor Author

srsudar commented Nov 29, 2025

Added some more tests to try and increase coverage.

@domoritz domoritz merged commit f74dc48 into vega:next Nov 30, 2025
4 checks passed
@domoritz
Copy link
Member

Thank you @srsudar

@github-actions
Copy link

🚀 PR was released in v2.5.0-next.10 🚀

This was referenced Nov 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants