Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 22 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ jobs:
run: node dist/bundle.js | grep -q "Vec2(4, 6)"
working-directory: examples/esbuild

# ─── Bun example ────────────────────────────────────────────────────────────
bun-example:
name: Bun example
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: ./.github/actions/setup-monorepo

- name: Build packages
run: bun run build

- name: Install Bun example dependencies
run: bun install
working-directory: examples/bun

- name: Assert Bun output
run: bun run start | grep -q "Vec2(4, 6)"
working-directory: examples/bun

# ─── Vite example ───────────────────────────────────────────────────────────
vite-example:
name: Vite example
Expand Down Expand Up @@ -145,7 +166,7 @@ jobs:
working-directory: examples/vite

- name: Assert bundle contains operator overload transformation
run: grep -rq 'Vec2\["+"\]\[0\]' examples/vite/dist/
run: grep -rq 'Vec2\["+"\](' examples/vite/dist/

# ─── Next.js example ────────────────────────────────────────────────────────
nextjs-example:
Expand Down
2 changes: 2 additions & 0 deletions Claude.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Always start a session by the openmemory MCP server. Always remind yourself to do this in conversation summaries between sessions. Always store important facts about the repository in the openmemory MCP server.

Where possible, use scripts from `package.json` files with Bun instead of directly running the commands or using NPM.
61 changes: 41 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Operator overloading for TypeScript.

![Sym.JS logo](https://github.com/DiefBell/boperators/blob/653ea138f4dcd1e6b4dd112133a4942f70e91fb3/logo.png)

`boperators` lets you define operator overloads (`+`, `-`, `*=`, `==`, etc.) on your TypeScript classes. It works by transforming your source code at the AST level using [ts-morph](https://ts-morph.com), replacing expressions like `v1 + v2` with the corresponding overload call `Vector3["+"][0](v1, v2)`.
`boperators` lets you define operator overloads (`+`, `-`, `*=`, `==`, etc.) on your TypeScript classes. It works by transforming your source code at the AST level using [ts-morph](https://ts-morph.com), replacing expressions like `v1 + v2` with the corresponding overload call `Vector3["+"](v1, v2)`.

## Quick Start

Expand All @@ -14,33 +14,53 @@ class Vector3 {
public y: number;
public z: number;

// Static operator: takes two parameters
static readonly "+" = [
(a: Vector3, b: Vector3) =>
new Vector3(a.x + b.x, a.y + b.y, a.z + b.z),
] as const;

// Instance operator: takes one parameter, uses `this`
readonly "+=" = [
function (this: Vector3, rhs: Vector3): void {
this.x += rhs.x;
this.y += rhs.y;
this.z += rhs.z;
},
] as const;
// Static operator: takes two parameters, returns a new instance
static "+"(a: Vector3, b: Vector3): Vector3 {
return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}

// Instance operator: takes one parameter, mutates in place
"+="(rhs: Vector3): void {
this.x += rhs.x;
this.y += rhs.y;
this.z += rhs.z;
}

// ...
}

// Usage - these get transformed automatically:
const v3 = v1 + v2; // => Vector3["+"][0](v1, v2)
v1 += v2; // => v1["+="][0].call(v1, v2)
const v3 = v1 + v2; // => Vector3["+"](v1, v2)
v1 += v2; // => v1["+="](v2)
```

> **Important:** Overload arrays **must** use `as const`. Without it, TypeScript widens the array type and loses individual function signatures, causing type errors in the generated code. boperators will error if `as const` is missing.

Overloads defined on a parent class are automatically inherited by subclasses. For example, if `Expr` defines `+` and `*`, a `Sym extends Expr` class can use those operators without redeclaring them.

## Multiple overloads per operator

A single operator can handle multiple type combinations using standard TypeScript method overload signatures. boperators registers each signature separately and dispatches to the correct one based on the operand types at each call site:

```typescript
class Vec2 {
// Vec2 * Vec2 → component-wise multiplication
static "*"(a: Vec2, b: Vec2): Vec2;
// Vec2 * number → scalar multiplication
static "*"(a: Vec2, b: number): Vec2;
static "*"(a: Vec2, b: Vec2 | number): Vec2 {
if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y);
return new Vec2(a.x * b, a.y * b);
}
}

const a = new Vec2(1, 2);
const b = new Vec2(3, 4);

a * b; // => Vec2["*"](a, b) → Vec2(3, 8) — routes to the Vec2 overload
a * 2; // => Vec2["*"](a, 2) → Vec2(2, 4) — routes to the number overload
```

The implementation method must accept the union of all overload parameter types; only the overload signatures (those without a body) are registered in the overload store.

## Publishing a library

If you are publishing a package that exports classes with operator overloads, consumers need to be able to import those classes for the transformed code to work. Run the following before publishing to catch any missing exports:
Expand Down Expand Up @@ -173,9 +193,10 @@ boperators/
- [x] Vite plugin
- [x] ESBuild plugin
- [x] Add support for Mozilla / V3 source map format, use in webpack plugin.
- [ ] Drop ts-morph dependency???
- [ ] ~~Drop ts-morph dependency???~~ NOPE
- [ ] A lot of logic in plugins, like `transformer` in the `tsc` plugin, that could be unified in core.
- [x] Update main package's README for new plugins
- [ ] Sub-package for using Symbols
- [ ] e2e test for Bun plugin and tsc plugin
- [ ] ???

Expand Down
35 changes: 18 additions & 17 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@boperators/cli",
"version": "0.2.1",
"version": "0.3.0",
"license": "MIT",
"description": "CLI tool for boperators - transforms TypeScript files with operator overloads.",
"repository": {
Expand Down Expand Up @@ -43,7 +43,7 @@
"@commander-js/extra-typings": "^13.0.0"
},
"peerDependencies": {
"boperators": "0.2.1",
"boperators": "0.3.0",
"typescript": ">=5.0.0 <5.10.0"
},
"devDependencies": {
Expand Down
8 changes: 3 additions & 5 deletions cli/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ export class Vec2 {
x: number;
y: number;
constructor(x: number, y: number) { this.x = x; this.y = y; }
static readonly "+" = [
(a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y),
] as const;
static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); }
}
`.trim();

Expand Down Expand Up @@ -153,7 +151,7 @@ describe("compile command", () => {
const { exitCode } = runCLI(["compile"], tmpDir);
expect(exitCode).toBe(0);
const js = fs.readFileSync(path.join(tmpDir, "dist", "usage.js"), "utf-8");
expect(js).toContain('Vec2["+"][0](a, b)');
expect(js).toContain('Vec2["+"](a, b)');
});

it("writes transformed TypeScript to --ts-out", () => {
Expand All @@ -164,7 +162,7 @@ describe("compile command", () => {
);
expect(exitCode).toBe(0);
const ts = fs.readFileSync(path.join(tsOut, "usage.ts"), "utf-8");
expect(ts).toContain('Vec2["+"][0](a, b)');
expect(ts).toContain('Vec2["+"](a, b)');
});

it("writes source map JSON to --maps-out", () => {
Expand Down
3 changes: 3 additions & 0 deletions examples/bun/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
55 changes: 55 additions & 0 deletions examples/bun/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/bun/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
preload = ["./node_modules/@boperators/plugin-bun/index.ts"]
13 changes: 13 additions & 0 deletions examples/bun/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@boperators/example-bun",
"private": true,
"scripts": {
"start": "bun run src/index.ts"
},
"devDependencies": {
"@boperators/plugin-bun": "file:../../plugins/bun",
"@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server",
"boperators": "file:../../package",
"typescript": "^5.0.0"
}
}
Loading