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

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1Ordering rules from most-specific to most-generic prevents broad patterns from swallowing narrower matches.
  2. 2A single generic matcher keeps browser and OS detection consistent by reusing the same lookup logic.
  3. 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.

Parsing a user-agent string with ordered rules — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code