Migrating from rut.js to rut.ts in 5 minutes
Migrate from rut.js, @fdograph/rut-utilities, or validate-rut to rut.ts (TypeScript-native, zero deps) — API map, codemods, and v4 differences.
The TypeScript and JavaScript ecosystem for Chilean RUT validation has been fragmented for a decade. Each package solved the core problem with a slightly different API surface, inconsistent error semantics, and no TypeScript types in the original source. rut.ts is API-compatible in spirit with the three most-installed packages — jlobos/rut.js, @fdograph/rut-utilities, and validate-rut — and moving over is mostly a find-and-replace. The migration pays back immediately in autocomplete, inline documentation, and a consistent safe-mode pattern that removes entire categories of defensive boilerplate.
What's different in rut.ts v4#
- Single safe-mode flag, library-wide. Every throwing function accepts
{ throwOnError: false }and returnsnullon failure. No try/catch, no per-package convention to memorise. - Strict mode for placeholders.
validate(value, { strict: true })rejects repeated-digit RUTs (11.111.111-1,22.222.222-2, …) that legacy validators happily accept. One option closes an entire bug class. - Zero runtime dependencies, types shipped in-package. No
@types/*companion to keep in sync, no transitive packages added to your bundle.
Why move#
The first reason is TypeScript nativity. rut.ts ships types as part of the package. There is no @types/rut-js companion to install, pin, and keep synchronized with the runtime package. When a @types/* package drifts behind the library it describes — a common occurrence in the ecosystem — your types lie to you silently. With rut.ts the types and the implementation move together, always.
The second reason is predictable error semantics. Legacy RUT libraries behave differently at the edges: some throw on non-string input, some return undefined, some return null, and a few return the string "Invalid". That inconsistency forces callers to write defensive code that is both verbose and fragile — a try/catch here, a truthiness check there, a typeof guard somewhere else. rut.ts offers a single answer to this problem: pass { throwOnError: false } to any function and you get null on failure instead of an exception. The pattern is uniform across the entire surface area.
The third reason is strict mode. The legacy validators accept placeholder RUTs — values like 11.111.111-1 that have a valid Modulo 11 verifier digit but were never assigned as real identities. Passing { strict: true } to validate() rejects those placeholders. The hardening post covers the full acceptance pipeline, but the short version is that one option closes a class of bugs that have no remedy in the older libraries.
The fourth reason is active maintenance. rut.ts has a published changelog, a semantic versioning contract, and ongoing releases. Several of the legacy packages have not had a commit in years. Pinning a dependency that nobody is watching is a supply-chain risk; it also means you absorb no improvements over time.
API mapping#
The table below maps the most common call patterns from each legacy library to their rut.ts equivalents.
| You wrote | Use instead |
|---|---|
rutHelpers.validateRut(value) (jlobos) | validate(value) |
rutHelpers.formatRut(value) (jlobos) | format(value) |
validateRut(value) (@fdograph/rut-utilities) | validate(value) |
formatRut(value) (@fdograph/rut-utilities) | format(value) |
cleanRut(value) (@fdograph/rut-utilities) | clean(value) |
dvCorrecto(body) (validate-rut) | calculateVerifier(body) |
| custom regex | isRutLike(value) for shape, validate(value) for semantics |
This table is a function-name mapping, not a guarantee of identical types or identical behavior at the edges. In normal cases — a string input that is either a valid or an invalid RUT — behavior is equivalent. But validate() returns false for any non-string input; some legacy validators threw a TypeError, some returned undefined, and at least one returned null. If you have call sites that test for truthiness rather than a strict boolean false, audit those paths before switching the import.
Search-and-replace recipe#
Start by finding every file that imports from a legacy package, then find every call site within those files. Running both queries up front gives you the full scope before you change anything.
rg -n "from ['\"]rut-?(js|chileno)['\"]|@fdograph/rut-utilities|validate-rut"
rg -n "(formatRut|validateRut|cleanRut|dvCorrecto)\("Commit the migration in two separate diffs. The first changes only import declarations: swap the old package name for rut.ts and align the imported identifiers. The second updates each call site. Keeping them separate makes the diff reviewable — the reviewer can follow the mechanical rename without mentally parsing unrelated surrounding context.
During the migration window, importing both the old package and rut.ts in the same codebase is safe. Both packages can coexist in node_modules. You can migrate module-by-module and ship intermediate states to production without any flag-flipping or compatibility shim. The only cost is the extra package in your dependency tree, which goes away once the last legacy import is replaced.
Tighten while you migrate#
The migration is an ideal moment to upgrade your validation posture at trust boundaries. Default to validate(value, { strict: true }) at every endpoint that accepts a RUT from an external caller. The legacy libraries' validators accept placeholder RUTs, so any existing code that copied their validation logic inherits that gap.
For acceptance paths that touch an external boundary — signup endpoints, identity verification flows, onboarding forms — add the acceptRutStrict() helper from the hardening post. It composes the type guard, the shape check, and the strict semantic check into a single function that returns null on any failure and the validated string on success.
This is also the right moment to drop hand-rolled regex shape checks. If your codebase has a home-grown regular expression to pre-filter RUT strings before passing them to a validator, replace it with isRutLike(). The regex post explains why regex is the wrong tool for both layers of the problem. Removing the custom regex eliminates a maintenance surface and makes the intent explicit.
Pitfalls#
jlobos's formatRut(value, digits) second argument has no direct equivalent in rut.ts. The jlobos implementation accepts a number of digits as a second parameter to control zero-padding. If you specifically want hyphen-only output without dot grouping, pass format(value, { dots: false }). That covers the most common use case for the second argument, but if you were using it to control padding, review those call sites explicitly.
Legacy validators accepted non-string inputs. Some would coerce numbers to strings internally; others accepted booleans. validate() returns false for any non-string. Audit call sites where the input might not be a string — particularly API handlers where JSON parsing produces a number for a field that the client was supposed to send as a string.
cleanRut() and clean() both uppercase the verifier. The letter K is normalized to uppercase in both libraries. If you stored RUTs cleaned by the legacy library and now compare them against rut.ts output, that behavior is consistent. That part of the migration is safe.
Error type expectations. If your code catches a specific error type thrown by a legacy library and branches on it, align your error handling to the rut.ts safe-mode pattern before swapping the import. Replace the try/catch block with a { throwOnError: false } call and a null check. Do this as part of the import-change diff so the error handling and the library switch land together.
Further reading#
- Install rut.ts —
pnpm add rut.tsornpm i rut.ts(zero dependencies) - Migration guide — drop-in mapping table and full reference
- Quick start — five minutes from install to a hardened acceptance gate
validate()referenceformat()referenceclean()referencecalculateVerifier()reference- The Modulo 11 algorithm for the Chilean RUT, explained — why a permissive legacy validator is not enough