npm package: @sirexelite/typedpaths
Description: Type-safe URL paths builder + parser with schema validation for path params and query params.
Latest version: 1.0.1

# TypedPaths

Type-safe URL paths builder + parser with schema validation for path params and query params.

- ✅ Only `/:name`, `/:name?`, `/:name*` are variables (everything else is literal)
- ✅ Path params + Query params validation (enum/int/slug/custom)
- ✅ `modify()` / `modifyQuery()` / `clearQuery()` / `removeQuery()`
- ✅ `read()` binds params from a URL (defaults to `location.href`)
- ✅ `readQueryOnly()` validates only query string
- ✅ `isValid()` checks if URL matches schema
- ✅ Optional segments (`?`) and rest segments (`*`)
- ✅ Vitest tests + vitest bench

## Install

```bash
npm i @sirexelite/typedpaths
# or
pnpm add @sirexelite/typedpaths
# or
yarn add @sirexelite/typedpaths
```

## Quick start
```
import { TypedPaths, T } from "@sirexelite/typedpaths";

const scheme = TypedPaths.create({
  "/:view/:subject_id?": {
    vars: [
      { name: "view", type: T.enum(["users", "posts"] as const) },
      { name: "subject_id", type: T.string() }
    ],
    queryParams: [
      { name: "tab", type: T.enum(["a", "b"] as const) },
      { name: "page", type: T.int() },
      { name: "q", type: T.string() }
    ],
    queryMode: "strict"
  },

  "/files/:path*": {
    vars: [{ name: "path", type: T.string() }],
    queryMode: "strict"
  },

  "/:page": {
    vars: [{ name: "page", type: T.slug(), validator: (v) => v !== "view" }],
    queryMode: "strict"
  }
} as const);
```

### Build URLs
#### Path only
```
scheme.view.modify({ view: "users" }).get();              // "/users"
scheme.subject_id.modify({ view: "users", subject_id: "1" }).get(); // "/users/1"
```

#### With query
```
scheme.view
.modify({ view: "users" })
.modifyQuery({ tab: "a", page: 2 })
.get(); // "/users?tab=a&page=2"
```

#### Clear / remove query params
```
const node = scheme.view
.modify({ view: "users" })
.modifyQuery({ q: "hello", page: 1, tab: "b" });

node.clearQuery().get();            // "/users"
node.clearQuery(["q"]).get();       // "/users?tab=b&page=1"
node.removeQuery("page").get();     // "/users?tab=b&q=hello"
```

#### Optional ```?``` and rest ```*```
```
// optional
scheme.subject_id.modify({ view: "users" }).get(); // "/users"

// rest
scheme.path.modify({ path: ["a", "b", "c"] }).get(); // "/files/a/b/c"
scheme.path.modify({ path: [] }).get();              // "/files"
```

### Read from URL
#### read(path?: string = location.href)
* Finds the best matching route (priority + order)
* Parses & validates path + query
* Returns a bound scheme where .get() keeps current values
```
const bound = scheme.read("https://x/users/abc?tab=b&page=3");

bound.match; // { pattern: "/:view/:subject_id?" }
bound.vars;  // { view: "users", subject_id: "abc", tab: "b", page: 3 }

bound.view.get();       // "/users?tab=b&page=3"
bound.subject_id.get(); // "/users/abc?tab=b&page=3"

// override bound values
bound.view.modify({ view: "posts" }).get(); // "/posts?tab=b&page=3"
```

### readQueryOnly(path?: string = location.href)
#### Validates only querystring using the first route whose query rules match.
```
const q = scheme.readQueryOnly("https://x/whatever?tab=a&page=2");
q.query; // { tab: "a", page: 2 }
q.match; // { pattern: "/:view/:subject_id?" }
```

#### isValid(path?: string = location.href)
```
scheme.isValid("https://x/users/abc?tab=a&page=2"); // true
scheme.isValid("https://x/users/abc?unknown=1");    // false (strict)
```

### Validation types

Built-in helpers:
* T.string()
* T.int()
* T.slug()
* T.enum([...])

#### Custom validator:
```
{ name: "id", type: T.string(), validator: (s) => s.length > 5 }
```

#### Route options
#### queryMode
* "strict": unknown query keys are rejected
* "loose": unknown query keys are ignored

#### prio
Higher prio matches earlier in read() / isValid().
```
"/:page": { prio: -100 }
```

### Benchmark
```
  name                                    hz     min     max    mean     p75     p99    p995    p999     rme  samples
· isValid() hit                   342,256.05  0.0023  2.7455  0.0029  0.0029  0.0062  0.0073  0.0116  ±1.21%   171150
· isValid() reject unknown query  216,793.35  0.0038  0.5789  0.0046  0.0047  0.0093  0.0110  0.0145  ±0.73%   108397
· read() bind                     102,927.77  0.0084  0.5754  0.0097  0.0096  0.0171  0.0195  0.0258  ±0.69%    51464
· build: path only                594,850.22  0.0014  0.4339  0.0017  0.0016  0.0030  0.0036  0.0074  ±0.74%   297426
· build: path + query             417,404.50  0.0020  4.8481  0.0024  0.0023  0.0042  0.0053  0.0097  ±2.02%   208703
· readQueryOnly()                 507,398.38  0.0016  0.5202  0.0020  0.0019  0.0039  0.0047  0.0085  ±0.72%   253700
· rest build                      710,507.88  0.0012  0.6027  0.0014  0.0014  0.0026  0.0029  0.0058  ±0.84%   355256
```