Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
efca8c6
first draft
fcrozatier Nov 14, 2025
a18d6d5
use Standard Schema
fcrozatier Nov 14, 2025
d190963
add a debug mode
fcrozatier Nov 15, 2025
a2a72aa
add static baseURL prop
fcrozatier Nov 16, 2025
fa91a41
make pattern public
fcrozatier Nov 17, 2025
39d1bf4
add index
fcrozatier Nov 17, 2025
ef64627
add tests
fcrozatier Nov 17, 2025
ac83033
add readme
fcrozatier Nov 17, 2025
f67d1b9
entrypoint
fcrozatier Nov 17, 2025
3f59b71
reorganize tests
fcrozatier Nov 20, 2025
3af158b
readme
fcrozatier Nov 20, 2025
8d6253b
compose and expose url pattern result
fcrozatier Nov 20, 2025
df731a5
add findBaseURL helper
fcrozatier Nov 20, 2025
7093523
test href
fcrozatier Nov 20, 2025
0b01354
remove only test
fcrozatier Nov 20, 2025
e020bc6
type safe hash
fcrozatier Nov 20, 2025
c97cbc9
improve type safety
fcrozatier Nov 20, 2025
42a61d3
format
fcrozatier Nov 20, 2025
1772664
unused test
fcrozatier Nov 20, 2025
3507d85
compute baseURL if needed
fcrozatier Nov 20, 2025
15c9e27
allow full URL match
fcrozatier Nov 20, 2025
fc75091
fix input type safety
fcrozatier Nov 20, 2025
45ed214
href handles wildcards
fcrozatier Nov 20, 2025
16271bf
handle regex groups
fcrozatier Nov 20, 2025
a023824
improve type safety
fcrozatier Nov 20, 2025
a048723
make lookaround global
fcrozatier Nov 21, 2025
1cd0876
more tests
fcrozatier Nov 21, 2025
7b85855
optional named group
fcrozatier Nov 21, 2025
b1a6290
final href validation
fcrozatier Nov 21, 2025
77cf177
optionally encodeURI
fcrozatier Nov 21, 2025
4734a9a
handle unmatched groups
fcrozatier Nov 21, 2025
da76c9c
doc
fcrozatier Nov 21, 2025
fb0aff2
unmatched groups
fcrozatier Nov 21, 2025
7f89232
move types
fcrozatier Nov 21, 2025
910de7d
replace all double //
fcrozatier Nov 21, 2025
6041707
lint
fcrozatier Nov 21, 2025
7839d8c
add test method
fcrozatier Nov 21, 2025
d4622c6
make internal
fcrozatier Nov 21, 2025
bbd81dd
always coerce to number
fcrozatier Nov 21, 2025
3d125ea
add examples
fcrozatier Nov 21, 2025
ca1e764
test more complex wildcards
fcrozatier Nov 21, 2025
4627022
fix optional wildcards
fcrozatier Nov 21, 2025
4b436d6
readme
fcrozatier Nov 21, 2025
65cec5b
format
fcrozatier Nov 24, 2025
cca7bbe
fix docs
fcrozatier Nov 24, 2025
234cb6f
format
fcrozatier Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions packages/typed-url-pattern/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# TypedURLPattern

A tiny TypeScript wrapper around the native
[URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) API
providing:

- **Type-safety** for your routes, endpoints and links
- **Parsing and validation** with [Standard Schema](https://standardschema.dev/)
- **Standard syntax**: it's just `URLPattern` under the hood (use the Platform)
- A typed `href()` inverse (create type-safe links)

## Install

## Common patterns

- **Typed named parameters**

```ts
import { TypedURLPattern } from "@f-stack/typed-url-pattern";
import * as z from "zod";

const route = new TypedURLPattern(
{ pathname: "/user/:name", baseURL: "https://example.com" },
{ params: z.object({ name: z.string() }) },
);

const match = route.match("/user/bob");

match?.params.name === "bob";
```

- **Typed wildcards**

Unnamed groups can be typed, parsed and validated in the order they appear

```ts
import { TypedURLPattern } from "@f-stack/typed-url-pattern";
import * as z from "zod";

const route = new TypedURLPattern(
{ pathname: "/assets/*/*.png", baseURL: "https://example.com" },
{ params: z.object({ 0: z.string(), 1: z.enum(["cake", "banana"]) }) },
);

const match = route.match("/assets/path/to/cake.png");

match?.params[0] === "path/to";
match?.params[1] === "cake";
```

- **Typed optional searchParams**

Use a `looseObject` to allow optional searchParams that are not specified in the
schema. This is useful when you don't control links to your page _eg_ search
engines adding `utm` searchParams etc.

```ts
import { TypedURLPattern } from "@f-stack/typed-url-pattern";
import * as z from "zod";

const route = new TypedURLPattern(
{ pathname: "/watch", baseURL: "https://example.com" },
{ searchParams: z.looseObject({ id: z.string() }) },
);

const match = route.match("/watch?id=abc&utm=utm_source");

match?.searchParams.id === "abc";

// utm has not been stripped since we use a looseObject
match?.searchParams.utm === "utm_source";
```

- **Parsing and validation**

Coerce strings extracted by `URLPattern` to numbers, booleans etc

```ts
import { TypedURLPattern } from "@f-stack/typed-url-pattern";
import * as z from "zod";

const route = new TypedURLPattern(
{ pathname: "/user/:id", baseURL: "https://example.com" },
{ params: z.object({ id: z.coerce.number() }) },
);

const match = route.match("/user/12");

match?.params.id === 12;
```

- **Default baseURL**

Use the static `baseURL` property to provide a sensible default to all your
patterns.

This allows to avoid the "relative URL without a base" `TypeError` common with
`URLPattern`

```ts
import { TypedURLPattern } from "@f-stack/typed-url-pattern";

// once
TypedURLPattern.baseURL = "https://example.com";

const route = new TypedURLPattern({ pathname: "/blog" });

route.test("https://example.com/blog") === true;
```

- **Typed href() inverse**

Build type safe links from your patterns and provided params.

The following demo showcases:

- typed params and searchParams substitution
- typed optional wildcards
- typed optional group delimiters
- typed optional searchParams

```ts
import { TypedURLPattern } from "@f-stack/typed-url-pattern";
import * as z from "zod";

const route = new TypedURLPattern(
{ pathname: "/blog{/*}?/:id{-:title}?", baseURL: "https://example.com" },
{
params: z.object({
id: z.coerce.number(),
title: z.string().optional(),
0: z.enum(["recipes", "trips"]).optional(),
}),
searchParams: z.looseObject({ page: z.coerce.number() }),
},
);

// without title but with wildcard
const href1 = route.href({
params: { id: 42, 0: "recipes" },
hash: "intro",
});

href1 === "https://example.com/recipes/42#intro";

// with title but without wildcard
const href2 = route.href({
params: { id: 42, title: "my-cake" },
searchParams: { page: 2 },
});

href2 === "https://example.com/42-mycake?page=2";
```

## API
12 changes: 12 additions & 0 deletions packages/typed-url-pattern/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@f-stack/typed-url-pattern",
"version": "0.1.0",
"license": "MIT",
"exports": {
".": "./src/typedURLPattern.ts"
},
"imports": {
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0",
"zod": "npm:zod@^4.1.12"
}
}
Loading