typescript
56 lines · 9 steps
Parsing and comparing semver strings in TypeScript
A regex-backed parser and a spec-faithful comparator that ranks version strings the way semver defines.
Explained by
highlit
1type Semver = {
2 major: number;
3 minor: number;
4 patch: number;
5 prerelease: string[];
6 build: string[];
7};
8
9const SEMVER_RE =
10 /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
11
12export function parseSemver(input: string): Semver {
13 const match = SEMVER_RE.exec(input.trim());
14 if (!match) {
15 throw new Error(`Invalid semver string: "${input}"`);
16 }
17 const [, major, minor, patch, prerelease, build] = match;
18 return {
19 major: Number(major),
20 minor: Number(minor),
21 patch: Number(patch),
22 prerelease: prerelease ? prerelease.split(".") : [],
23 build: build ? build.split(".") : [],
24 };
25}
26
27function comparePrerelease(a: string[], b: string[]): number {
28 if (a.length === 0 && b.length > 0) return 1;
29 if (a.length > 0 && b.length === 0) return -1;
30
31 const len = Math.min(a.length, b.length);
32 for (let i = 0; i < len; i++) {
33 const x = a[i];
34 const y = b[i];
35 const xn = /^\d+$/.test(x);
36 const yn = /^\d+$/.test(y);
37 if (xn && yn) {
38 const diff = Number(x) - Number(y);
39 if (diff !== 0) return Math.sign(diff);
40 } else if (xn !== yn) {
41 return xn ? -1 : 1;
42 } else if (x !== y) {
43 return x < y ? -1 : 1;
44 }
45 }
46 return Math.sign(a.length - b.length);
47}
48
49export function compareSemver(a: string, b: string): -1 | 0 | 1 {
50 const va = parseSemver(a);
51 const vb = parseSemver(b);
52 for (const key of ["major", "minor", "patch"] as const) {
53 if (va[key] !== vb[key]) return va[key] < vb[key] ? -1 : 1;
54 }
55 return comparePrerelease(va.prerelease, vb.prerelease) as -1 | 0 | 1;
56}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Capture groups map cleanly onto a structured record via array destructuring.
- 2Semver comparison is lexicographic on numeric fields, then a special-cased prerelease ordering.
- 3A version without a prerelease outranks one that has it, and numeric identifiers compare numerically.
Related explainers
typescript
import { Component, computed, signal } from '@angular/core'; import { CdkTableModule } from '@angular/cdk/table'; import { CdkScrollableModule } from '@angular/cdk/scrolling';
Paginating a CDK table with Angular signals
signals
computed-state
pagination
Intermediate
9 steps
typescript
import { Component } from '@angular/core'; import { trigger, transition,
Staggered list animations in Angular
animations
stagger
enter-leave
Intermediate
10 steps
typescript
type Handler = (event: KeyboardEvent) => void; interface Binding { combo: string;
Building a keyboard shortcut manager in TypeScript
event-handling
normalization
closures
Intermediate
7 steps
javascript
const TOKEN_SPECS = [ ["comment", /^\/\/[^\n]*|^\/\*[\s\S]*?\*\//], ["string", /^"(?:\\.|[^"\\])*"|^'(?:\\.|[^'\\])*'|^`(?:\\.|[^`\\])*`/], ["number", /^0[xX][\da-fA-F]+|^\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/],
Building a syntax highlighter tokenizer
tokenizer
regular-expressions
lexing
Intermediate
8 steps
typescript
type MatchSegment = { text: string; matched: boolean; };
Splitting text into highlighted match segments
regex
string-matching
text-highlighting
Intermediate
8 steps
typescript
import { Component, input, computed } from '@angular/core'; function toNumber(value: number | string): number { return typeof value === 'number' ? value : parseFloat(value);
Signal inputs and computed in Angular
signals
reactivity
derived-state
Intermediate
5 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/parsing-and-comparing-semver-strings-in-typescript-explained-typescript-c36b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.