typescript
47 lines · 9 steps
Parsing a user-agent string with ordered rules
Ordered regex tables and a shared matcher turn a raw user-agent string into structured browser, OS, and device info.
Explained by
highlit
1interface UserAgentInfo {
2 browser: { name: string; version: string };
3 os: { name: string; version: string };
4 device: 'mobile' | 'tablet' | 'desktop';
5}
6
7const BROWSER_RULES: Array<[RegExp, string]> = [
8 [/Edg\/([\d.]+)/, 'Edge'],
9 [/OPR\/([\d.]+)/, 'Opera'],
10 [/Firefox\/([\d.]+)/, 'Firefox'],
11 [/Chrome\/([\d.]+)/, 'Chrome'],
12 [/Version\/([\d.]+).*Safari/, 'Safari'],
13 [/MSIE ([\d.]+)|rv:([\d.]+)\) like Gecko/, 'Internet Explorer'],
14];
15
16const OS_RULES: Array<[RegExp, string]> = [
17 [/Windows NT ([\d.]+)/, 'Windows'],
18 [/Mac OS X ([\d_.]+)/, 'macOS'],
19 [/Android ([\d.]+)/, 'Android'],
20 [/(?:iPhone|iPad); CPU (?:iPhone )?OS ([\d_]+)/, 'iOS'],
21 [/Linux/, 'Linux'],
22];
23
24function matchRule(ua: string, rules: Array<[RegExp, string]>) {
25 for (const [pattern, name] of rules) {
26 const m = ua.match(pattern);
27 if (m) {
28 const version = (m[1] ?? m[2] ?? '').replace(/_/g, '.');
29 return { name, version };
30 }
31 }
32 return { name: 'Unknown', version: '' };
33}
34
35export function parseUserAgent(ua: string): UserAgentInfo {
36 const browser = matchRule(ua, BROWSER_RULES);
37 const os = matchRule(ua, OS_RULES);
38
39 let device: UserAgentInfo['device'] = 'desktop';
40 if (/iPad|Tablet|(?=.*Android)(?!.*Mobile)/.test(ua)) {
41 device = 'tablet';
42 } else if (/Mobi|iPhone|Android.*Mobile/.test(ua)) {
43 device = 'mobile';
44 }
45
46 return { browser, os, device };
47}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Ordering rules from most-specific to most-generic prevents broad patterns from swallowing narrower matches.
- 2A single generic matcher keeps browser and OS detection consistent by reusing the same lookup logic.
- 3Deriving a union type from an interface field keeps assignable values in lockstep with the contract.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 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-a-user-agent-string-with-ordered-rules-explained-typescript-965e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.