Skip to content

refactor: remove ts-toolbelt dependency#53

Merged
regevbr merged 2 commits into
masterfrom
remove-ts-toolbelt-dependency
Jul 15, 2026
Merged

refactor: remove ts-toolbelt dependency#53
regevbr merged 2 commits into
masterfrom
remove-ts-toolbelt-dependency

Conversation

@regevbr

@regevbr regevbr commented Feb 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Remove ts-toolbelt dependency by replacing all utilities with native TypeScript built-ins and custom implementations
  • Maintain 100% type safety with no runtime behavior changes
  • Reduce bundle size by eliminating the only runtime dependency

Changes

New file: src/types/utils.ts

Custom type utilities replacing ts-toolbelt:

  • Primitive, BuiltIn - Type categories
  • List, Cast, Keys, Compute - Any/list utilities
  • Split, Join - String utilities
  • Path, FilterNever, NonNullableFlat - Object utilities
  • RequireOnlyOne - Replacement for Object.Either

Modified files

File Changes
src/types/paths.ts Replace ts-toolbelt imports with ./utils
src/types/required.ts Replace Misc.BuiltInBuiltIn
src/types/evaluator.ts Replace ts-toolbelt imports, re-export RequireOnlyOne
src/types/expressionParts.ts Replace imports, use native Parameters
src/lib/helpers.ts Replace Any.KeyPropertyKey
src/lib/engine.ts Add type assertion for RuleDefinition narrowing
src/test/types/expressionParts.ts Replace Any.ComputeCompute
package.json Remove ts-toolbelt dependency

Test plan

  • All 166 unit tests pass
  • 100% code coverage maintained
  • All TypeScript definition tests pass
  • Linting passes
  • Full CI pipeline passes (pnpm run ci)

🤖 Generated with Claude Code

Replace all ts-toolbelt utilities with native TypeScript built-ins and
custom implementations while maintaining 100% type safety.

Changes:
- Create src/types/utils.ts with custom type utilities (Primitive,
  BuiltIn, List, Cast, Keys, Compute, Split, Join, Path, FilterNever,
  NonNullableFlat, RequireOnlyOne)
- Replace ts-toolbelt imports in paths.ts, required.ts, evaluator.ts,
  and expressionParts.ts
- Replace Any.Key with PropertyKey in helpers.ts
- Add type assertion in engine.ts for RuleDefinition narrowing
- Update test file to use custom Compute type
- Remove ts-toolbelt from dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@qltysh

qltysh Bot commented Feb 1, 2026

Copy link
Copy Markdown

❌ 4 blocking issues (4 total)

Tool Category Rule Count
radarlint-js Lint Remove this type without members or change this type intersection. 3
radarlint-js Lint 'unknown' is overridden by other types in this intersection type. 1

Comment thread src/types/utils.ts Outdated
Comment thread src/types/utils.ts Outdated
Comment thread src/types/utils.ts Outdated
Comment thread src/types/utils.ts Outdated
@qltysh

qltysh Bot commented Feb 1, 2026

Copy link
Copy Markdown

Qlty

Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
src/lib/helpers.ts100.0%
Coverage rating: A Coverage rating: A
src/lib/engine.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

…add type benchmark

Follow-up review of the ts-toolbelt removal, focused on correctness, exact
behavioural parity with the old ts-toolbelt types, and type-check performance.

- Path: add a materialization boundary (`_Path` + `_Path<..> extends infer X ? X`)
  so relating two `Path<>` instantiations resolves to concrete leaves instead of
  recursing structurally through nested `_Path`. Fixes TS2321 "Excessive stack
  depth comparing types 'Path<?, P>'". A cross-context `or` that the ts-toolbelt
  types compiled at ~4.1M instantiations / TS2321 now compiles at ~239k.

- required.ts NonNullable (deep): strip null/undefined *before* the `extends
  object` guard, so nullables under `SomeObject | undefined` are deep-stripped
  again (a regression vs the released ts-toolbelt behaviour). Guarded by
  src/test/types/validationContext.test-d.ts.

- utils.ts: drop the erased `& {}` / `& unknown` "prettify" intersections flagged
  by radarlint (S4335 / S6571). RequireOnlyOne keeps a bare homomorphic
  mapped-type prettify, which is structurally identical to ts-toolbelt's
  Object.Either (strict) while staying lint-clean.

- paths.ts: drop the `NonNullableFlat<[...P, k]>` wrapper (a tuple of PropertyKeys
  is never nullable, so it was a no-op) and the now-unused NonNullableFlat export.

- Add a cross-platform type-instantiation benchmark (src/test/benchmark.spec.ts)
  that compiles a heavy fixture with `tsc --extendedDiagnostics` and fails on
  excessive-depth errors or if instantiations regress past budget.

Verified structurally identical to the ts-toolbelt output across a battery of
contexts (Expression / ExpressionParts / ValidationContext / PropertyCompareOps /
StringPaths / Paths / Rule) and every removed utility. lint, compile, tsd and
100% coverage all pass.

🤖 Generated with [Claude Code](https://claude.ai/code)
@rubnogueira

Copy link
Copy Markdown

Follow-up review: type correctness, exact parity & TypeScript performance

Pushed as ae40639. This is a deep review of the ts-toolbelt removal focused on exact behavioural parity with the old types, type-check performance, and regression safety.

TL;DR — TypeScript performance (identical cross-context or logic)

Compiling the same deep cross-context { or: [a, b] } assignment (tsc --extendedDiagnostics):

Metric OLD (ts-toolbelt) NEW (this branch) Δ
Result TS2321 (excessive depth) ✅ compiles
Types 1,018,577 61,698 ~16.5× fewer
Instantiations 4,147,372 238,969 ~17.4× fewer
Memory ~1,100 MB ~216 MB ~5× less
Check time 9.77 s 1.00 s ~10× faster

The TS2321: Excessive stack depth comparing types 'Path<?, P>' that surfaced downstream is reproduced by the old ts-toolbelt Object.Path — it was not introduced here; this branch removes it.

Root cause & fix — Path

ts-toolbelt's Object.Path materialised its result (_Path<..> extends infer X ? Cast<X,any>). The first cut of utils.ts dropped that boundary, so relating two Path<> instances recursed structurally through every nested step → TS2321 / TS2589 on deep contexts. Restored a materialization boundary:

type _Path<O, P extends readonly PropertyKey[]> =
  P extends [] ? O
  : P extends [infer H extends PropertyKey, ...infer T extends PropertyKey[]] ? _Path<At<O, H>, T>
  : never;
export type Path<O, P extends readonly PropertyKey[]> = _Path<O, P> extends infer X ? X : never;

I also measured the ts-toolbelt-style object-indexed iteration ({0,1}[Extends<..>]) and a per-key Path cache — both were worse or neutral (the iteration form reproduces TS2321 at ~2M instantiations; manual caching doesn't help because TS already memoises identical instantiations). The plain destructuring + materialization above is the fastest form that stays exact.

Correctness fix — deep NonNullable (ValidationContext)

The rewrite guarded recursion with O[K] extends object, which is false for SomeObject | undefined, so nullables under an optional/nullable object property were left un-stripped (a regression vs the released ts-toolbelt behaviour). Fixed by stripping null before the guard, and added src/test/types/validationContext.test-d.ts (fails on the old code, passes now).

Bot comments (radarlint S4335 / S6571) — resolved

The 4 flagged & {} / & unknown intersections are erased no-ops. Removed them; RequireOnlyOne keeps a bare homomorphic mapped-type prettify that is structurally identical to ts-toolbelt's Object.Either (strict) while staying lint-clean.

ts-toolbelt → native: old vs new (all verified Equal)

Removed ts-toolbelt usage Replacement Verified
Any.Key PropertyKey ✅ exact
Any.Cast Cast ✅ exact
Any.Keys Keys ✅ exact
Any.Compute (ComputeRaw) Compute (bare mapped type) ✅ exact
List.List List ✅ exact
List.Length<P> P['length'] ✅ exact
Misc.BuiltIn BuiltIn ✅ exact
Misc.Primitive Primitive ✅ exact
Union.Exclude Exclude (native) ✅ exact
Union.NonNullable NonNullable / globalThis.NonNullable ✅ exact
Function.Parameters Parameters (native) ✅ exact
String.Split / String.Join Split / Join ✅ exact
Object.Path Path (materialized) ✅ exact
Object.Either RequireOnlyOne ✅ exact
Object.Filter<_, never, 'equals'> FilterNever ✅ exact
Object.NonNullable NonNullableFlat (removed — was a no-op on key tuples) n/a

Parity method: reinstalled ts-toolbelt locally and asserted Equal<Old, New> (invariant identity, not just assignability) for the whole public surface — Expression, ExpressionParts, ValidationContext, PropertyCompareOps, StringPaths, Paths, Rule — across a battery of contexts (optional/nullable objects, arrays, tuples, Record index signatures, readonly, any/unknown, union objects, depth‑11 chains, Moment+Ignore, empty/single‑key). All identical. (ts-toolbelt is not re-added as a dependency.)

Extra optimisation

paths.ts wrapped every path leaf in NonNullableFlat<[...P, k]>; the tuple is PropertyKeys (never nullable), so it was a no-op. Dropped it (and the now-unused export) — small per-leaf instantiation + memory saving, output unchanged.

Regression guard — src/test/benchmark.spec.ts

New cross-platform benchmark: compiles a heavy fixture with tsc --extendedDiagnostics and fails if there's any excessive-depth error or instantiations regress past budget. Reverting Path to a ts-toolbelt-style encoding trips it (TS2321 + ~2M instantiations); current baseline ~321k.

Verification

lint, compile, tsd (incl. new regression test), 100% coverage (167 tests), and pnpm run ci all pass.

🤖 Generated with Claude Code

@rubnogueira

Copy link
Copy Markdown

@qltysh resume

@rubnogueira rubnogueira self-requested a review July 14, 2026 18:23
@regevbr regevbr merged commit 87431d3 into master Jul 15, 2026
10 checks passed
@regevbr regevbr deleted the remove-ts-toolbelt-dependency branch July 15, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants