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
php
<?php namespace App\Validation;
Building a reusable address form validator in PHP
validation
error-accumulation
regex
Intermediate
9 steps
typescript
export function isValidCardNumber(input: string): boolean { const digits = input.replace(/[\s-]/g, ""); if (!/^\d{12,19}$/.test(digits)) {
Validating card numbers with the Luhn check
luhn-algorithm
checksum
input-validation
Intermediate
7 steps
rust
pub fn normalize_path(input: &str) -> String { let is_absolute = input.starts_with('/'); let has_trailing_slash = input.len() > 1 && input.ends_with('/'); let mut stack: Vec<&str> = Vec::new();
Normalizing filesystem paths in Rust
string-processing
stack
path-manipulation
Intermediate
8 steps
rust
use axum::{ extract::Query, response::IntoResponse, Json,
Parsing query strings in Axum handlers
deserialization
query-parameters
defaults
Intermediate
7 steps
php
<?php declare(strict_types=1);
Normalizing human names in PHP
unicode
text-normalization
transliteration
Intermediate
8 steps
typescript
import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, timer, throwError } from 'rxjs'; import { switchMap, takeWhile, filter, take, catchError } from 'rxjs/operators';
Polling a job until it finishes in Angular
rxjs
polling
observables
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.