[ Language & values ]
@sdxc/semver
SemVer 2.0.0 parsing, precedence ordering and range-free version comparisons
- Installs with
- @sdxc/result
- Used by
- blog
- Source
- packages/semver
SemVer 2.0.0 parsing, precedence ordering and range-free version comparisons.
Installation
npm add @sdxc/semver
parse() answers a Result from @sdxc/result, which installs alongside this package.
Usage
Compare two versions
import { satisfies } from "@sdxc/semver";
satisfies("2.0.0", ">", "1.99.99"); // true
satisfies("1.9.0", "^", "1.4.2"); // true, same major
satisfies("1.5.0", "~", "1.4.2"); // false, a later minor
satisfies("nightly", ">", "1.0.0"); // false, not a version
Sort a list of versions
import { compare } from "@sdxc/semver";
["2026.10.1", "2026.9.4", "2026.9.30"].sort(compare);
// ["2026.9.4", "2026.9.30", "2026.10.1"]
["1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2"].sort(compare);
// ["1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"]
Read a version's elements
import { parse } from "@sdxc/semver";
import { isFailure } from "@sdxc/result";
let result = parse("v2.1.0-rc.3+build.9");
if (isFailure(result)) {
result.error.message; // Invalid version: "..."
} else {
result.data; // { major: 2, minor: 1, patch: 0, prerelease: ["rc", "3"] }
}
API
parse(text: string): Result<SemVer, InvalidSemVerError>
Reads a version into its elements, or fails with an InvalidSemVerError. The grammar is SemVer 2.0.0 plus an optional leading v; build metadata is dropped, and padded elements such as 01.2.3 are rejected.
compare(a: string, b: string): number
Orders two version strings by precedence, ready for Array.prototype.sort: negative when a comes first, positive when b does, 0 when they rank equally. Text that is not a version ranks below every version and ties with other such text, so any list sorts in one call.
satisfies(value: string, comparison: SemVerComparison, against: string): boolean
Whether value stands in the named relation to against. Either side that is not a version answers false. A prerelease takes part by precedence alone, so 1.2.4-rc.1 satisfies ^ 1.2.3.
| Comparison | Holds when |
|---|---|
= | The two rank equally |
!= | The two rank differently |
< | value precedes against |
<= | value precedes against or ranks equally |
> | value follows against |
>= | value follows against or ranks equally |
~ | value is at least against and shares its major and minor |
^ | value is at least against and keeps its left-most non-zero element |
InvalidSemVerError
The error inside a failed parse(). text holds the rejected input verbatim, name is "InvalidSemVerError", and message is Invalid version: "<text>" with the text JSON-quoted so whitespace and empty strings stay visible.
Precedence
Major, minor and patch compare as numbers, so 1.10.0 follows 1.9.0; build metadata never counts. A prerelease precedes its release. Prerelease identifiers compare one by one: numerically when both are numeric, by character otherwise, a numeric identifier below a textual one, and a shorter list first when the other continues (1.0.0-alpha precedes 1.0.0-alpha.1).
Types
SemVer
interface SemVer {
major: number;
minor: number;
patch: number;
prerelease: string[];
}
A parsed version. prerelease holds the dot-separated identifiers after the -, empty for a release.
SemVerComparison
type SemVerComparison = "=" | "!=" | "<" | "<=" | ">" | ">=" | "~" | "^";
The comparisons satisfies() accepts, a closed union that stores and validates as plain data.
Pattern: Gating A Feature On A Client Version
Keep the rule as data so an operator can change it without a deploy.
import type { SemVerComparison } from "@sdxc/semver";
import { satisfies } from "@sdxc/semver";
interface VersionRule {
comparison: SemVerComparison;
version: string;
}
function allows(rule: VersionRule, clientVersion: string): boolean {
return satisfies(clientVersion, rule.comparison, rule.version);
}
allows({ comparison: "^", version: "2.0.0" }, "2.4.1"); // true
allows({ comparison: ">=", version: "3.0.0" }, "2.4.1"); // false
allows({ comparison: ">=", version: "3.0.0" }, "unknown"); // false
Pattern: Picking The Newest Stable Version
Sort with compare(), then skip prereleases by reading the parsed elements. Entries that are not versions sort to the front and never win.
import { compare, parse } from "@sdxc/semver";
import { isSuccess } from "@sdxc/result";
function newestStable(versions: string[]): string | undefined {
return [...versions]
.sort(compare)
.reverse()
.find((version) => {
let result = parse(version);
return isSuccess(result) && result.data.prerelease.length === 0;
});
}
newestStable(["latest", "1.4.0", "2.0.0-rc.1", "1.10.0"]); // "1.10.0"
Pattern: Checking Whether An Upgrade Is Compatible
^ answers "same compatibility line, and not a downgrade", including the 0.x and 0.0.x cases.
import { satisfies } from "@sdxc/semver";
function isCompatibleUpgrade(from: string, to: string): boolean {
return satisfies(to, "^", from);
}
isCompatibleUpgrade("1.4.2", "1.9.0"); // true
isCompatibleUpgrade("1.4.2", "2.0.0"); // false
isCompatibleUpgrade("0.2.3", "0.3.0"); // false, below 1.0.0 the minor is the line
Pattern: Reporting Invalid Entries
Validate input once with parse() and name each rejection from the error's text.
import type { SemVer } from "@sdxc/semver";
import { parse } from "@sdxc/semver";
import { isFailure } from "@sdxc/result";
function readVersions(input: string[]): { versions: SemVer[]; rejected: string[] } {
let versions: SemVer[] = [];
let rejected: string[] = [];
for (let text of input) {
let result = parse(text);
if (isFailure(result)) rejected.push(result.error.text);
else versions.push(result.data);
}
return { versions, rejected };
}
readVersions(["1.0.0", "v2.1.0", "1.2", " 3.0.0"]);
// two SemVer values, rejected: ["1.2", " 3.0.0"]