ts-operator-overload
v0.1.6
Published
Typed operator overloading for TypeScript using `// @operator...` annotations
Downloads
815
Maintainers
Readme
ts-operator-overload
Typed operator overloading for TypeScript using // @operator... annotations
Write normal operators in your code (a + b, ~x, x += y) and let the transformer/plugin rewrite them to method
calls with type-aware resolution
Table of Contents
- What This Is
- Quick Start (2 Minutes)
- How It Chooses Which Method to Call
- Supported Operators
- Valid Annotation Signatures
- TSConfig / tsserver Plugin
- Programmatic API
- Diagnostics and Custom Errors
- Examples
- IDE Quick Setup (VS Code and JetBrains)
- Troubleshooting
- Development and Tests
What This Is
ts-operator-overload lets you define operator behavior on your own types with method annotations
Example:
class Vec {
constructor(public x: number) {
}
// @operator+
add(y: number): Vec {
return new Vec(this.x + y)
}
}
const v = new Vec(1)
const r = v + 2 // rewritten to v.add(2)This package supports:
- compile-time rewriting for
tsctransformer usage - editor-time diagnostics and type feedback through a
tsserverplugin
Quick Start (2 Minutes)
1) Install
npm install ts-operator-overload2) Annotate methods
class A {
constructor(public x: number) {
}
// @operator+
add(y: number): string {
return `Adding ${y} to ${this.x}`
}
}
const c = new A(5)
const d = c + 5 // becomes c.add(5)3) Enable plugin in tsconfig.json for editor behavior
{
"compilerOptions": {
"plugins": [
{
"name": "ts-operator-overload",
"mode": "shadow"
}
]
}
}How It Chooses Which Method to Call
For a binary expression like:
A + Bthe resolver checks in this order and picks the first match:
A.add(B)A.add(A, B)as a static methodB.add(A, B)as a static method
So left-operand methods are preferred before reverse/right-side fallback
For unary operators, the resolver checks:
- zero-arg form, for example
x.unaryBitNot() - one-arg self form, for example
x.unaryBitNot(x)
For compound assignment and increments:
x += yresolves as assignment using@operator+x++and++xresolve as assignment using@operator+with1- similarly
-=and--resolve via@operator-
Smart fallbacks are included for missing operators:
- if
a != bis missing buta == bexists, it rewrites to!(a == b) - for comparisons, it prefers single-op negation fallbacks like
a >= b->!(a < b) - if
a - bis missing buta + ...exists, it rewrites asa + (-b)(using native or overloaded unary-)
Supported Operators
Binary operators
@operator+@operator-@operator*@operator/@operator%@operator**@operator==@operator!=@operator===@operator!==@operator>@operator>=@operator<@operator<=@operator&&@operator||@operator??@operator&@operator|@operator^@operator<<@operator>>@operator>>>
Unary operators
@operator+@operator-@operator!@operator~
Compound and increment forms
Mapped through annotated binary operators:
+=,-=,*=,/=,%=,**=&&=,||=,??=&=,|=,^=,<<=,>>=,>>>=++,--
Valid Annotation Signatures
The annotation validator enforces accepted method shapes
Binary form (@operator+, etc)
Accepted:
// one-arg left form
add(b: number): Result
// two-arg left form (must be static)
static add(a: ThisType, b: number): Result
// two-arg right/reverse form (on rhs type, must be static)
static add(a: number, b: ThisType): ResultUnary form (@operator!, @operator~, unary + and -)
Accepted:
unaryNot(): Result
unaryNot(a: ThisType): ResultRejected example (custom error):
// @operator~
unaryBitNot(a: Ops, b: number): string // invalidTSConfig / tsserver Plugin
Set in compilerOptions.plugins:
{
"compilerOptions": {
"plugins": [
{
"name": "ts-operator-overload",
"mode": "shadow"
}
]
}
}Modes:
suppresssuppresses operator diagnostics when a valid overload existsshadowbuilds a rewritten virtual program for richer types, completions, and inlay hintshybridresponds fast likesuppress, then warms shadow typing after edits settle
Performance options for shadow mode:
shadowScope:"file"(default, rewrites only requested file) or"project"maxShadowFiles: auto-fallback threshold for large projects (0 disables threshold)autoFallbackToSuppress:trueby defaultshadowFeatures: selectively enable expensive editor features
Hybrid options:
hybridDebounceMs: wait time after edits before warm shadow rebuild (default800)hybridWarmOn: per-feature shadow enablement in hybrid modehybridMaxBuildMs: optional budget for a warm rebuild before it is treated as failedhybridFailureDisableAfter: disable hybrid warmup temporarily after repeated failureshybridCooldownMs: cooldown duration before retrying warmups
Example hybrid config:
{
"compilerOptions": {
"plugins": [
{
"name": "ts-operator-overload",
"mode": "hybrid",
"shadowScope": "file",
"maxShadowFiles": 400,
"hybridDebounceMs": 800,
"hybridWarmOn": {
"quickInfo": true,
"completions": true,
"completionDetails": true,
"inlayHints": false,
"diagnostics": false
}
}
]
}
}Large-project recommended config (fast editor feedback):
{
"compilerOptions": {
"plugins": [
{
"name": "ts-operator-overload",
"mode": "shadow",
"shadowScope": "file",
"maxShadowFiles": 400,
"autoFallbackToSuppress": true,
"shadowFeatures": {
"diagnostics": true,
"quickInfo": true,
"completions": false,
"completionDetails": false,
"inlayHints": false
}
}
]
}
}If you only want fast error suppression and don't need shadow-typed editor features, use "mode": "suppress".
Programmatic API
From index.d.ts:
createOperatorOverloadTransformer(program, options?)createTscTransformer(program, options?)shouldSuppressOperatorDiagnostic(diagnostic, program, options?)tsserverPlugin
Current option field:
allowRightOperand?: boolean
Diagnostics and Custom Errors
Standard TS operator diagnostics may appear when no valid overload is found, such as:
TS2362TS2363TS2365TS2367
This package also emits custom annotation validation diagnostics:
TS93001(source:ts-operator-overload)
Typical causes:
- unsupported
@operator...token - invalid arity for unary/binary operator
- invalid two-arg form that does not include the owning type
Examples
Reverse add fallback
class A {
constructor(public x: number) {
}
}
class B {
constructor(public y: number) {
}
// @operator+
add(a: A, b: B): string {
return `${a.x}+${b.y}`
}
}
const a = new A(1)
const b = new B(2)
const out = a + b // falls back to b.add(a, b)Equality overload
class EqClass {
// @operator==
eq(y: number): string {
return `${y}`
}
}
const r = new EqClass() == 5 // rewritten to .eq(5), type string
r.split("")IDE Quick Setup (VS Code and JetBrains)
Use each project's local TypeScript version so the plugin behavior matches the project dependencies.
VS Code (fast path)
- Open the target project folder (example:
test/vscode-basic) - Install dependencies in that folder
- Point VS Code to local TypeScript at
node_modules/typescript/lib - Run
TypeScript: Select TypeScript Versionand chooseUse Workspace Version
Example commands:
npm --prefix test/vscode-basic installIf needed, set this in .vscode/settings.json inside that project:
{
"typescript.tsdk": "./node_modules/typescript/lib"
}JetBrains IDEs (WebStorm / IntelliJ)
- Open the target project folder in the IDE
- Install dependencies in that folder
- Go to
Settings > Languages & Frameworks > TypeScript - Enable TypeScript service
- Set
TypeScript packageto the local path:<project>/node_modules/typescript
Example command:
npm --prefix test/nextjs-basic installThis ensures editor diagnostics and types use the project's own TypeScript instead of a global version.
Troubleshooting
- operator not rewritten: check method annotation and signature shape
- type still looks native in editor: ensure plugin is loaded and mode is
shadow - unexpected custom error
TS93001: method has an invalid operator signature - right-side fallback not used: left matches always win and number-like rhs fallback is restricted
Development and Tests
Run full matrix:
npm install
npm testIncluded test areas:
test/tsc-*transformer output and expected-error casestest/tsserver-smokediagnostic suppression behaviortest/tsserver-typedquick-info, inlay, and completion type propagationtest/eslint-basiceslint integration and TypeScript parsing/linting compatibilitytest/vite-basicandtest/nextjs-basicintegration builds
