Skip to content

Brand RouteUrl type to distinguish it from plain strings#877

Merged
bakerkretzmar merged 3 commits intotighten:2.xfrom
pataar:improve/route-url-branded-type
Mar 6, 2026
Merged

Brand RouteUrl type to distinguish it from plain strings#877
bakerkretzmar merged 3 commits intotighten:2.xfrom
pataar:improve/route-url-branded-type

Conversation

@pataar
Copy link
Copy Markdown
Contributor

@pataar pataar commented Mar 5, 2026

Summary

The RouteUrl branded type I added a while ago used a plain alias. I've since learned that using a unique symbol is the recommended approach, so this PR improves the branding.

  • Now RouteUrl uses a unique symbol brand — still assignable to string, but a plain string can't be assigned to RouteUrl
  • Added a type test verifying that a plain string is not assignable to RouteUrl

Why unique symbol?

Using unique symbol as the brand key is the recommended pattern for branded types in TypeScript because:

  • Each brand is globally unique, even across modules — no risk of accidental type collisions
  • The brand property is hidden from IntelliSense, keeping autocomplete clean
  • Zero runtime overheaddeclare const only exists in the type system

See also: Preventing Accidental Interchangeability in TypeScript

Example

const url: RouteUrl = route('posts.show', 1); // ✅ works
const url: RouteUrl = '/posts/1';             // ❌ type error

const str: string = route('posts.show', 1);   // ✅ still works, RouteUrl extends string

Test plan

  • Existing type tests pass
  • New @ts-expect-error test confirms a plain string is not assignable to RouteUrl
  • All runtime tests pass (npx vitest --typecheck)

pataar and others added 3 commits March 5, 2026 16:08
RouteUrl was a plain alias for `string`, so there was no type-level
distinction between a URL produced by `route()` and any arbitrary string.

Now RouteUrl is a branded type: it's still assignable to `string`, but a
plain `string` can't be assigned to `RouteUrl` without going through
`route()`.
@bakerkretzmar
Copy link
Copy Markdown
Collaborator

Very cool. Re-explaining your code example for my own understanding, and for when I inevitably come back to this to remind myself how it works: now, anything explicitly typed as RouteUrl must come from route(). If I have a variable that's usually a Ziggy-generated route URL but might also be a manually constructed URL string, I have to use string, if I use RouteUrl that means I'm intentionally restricting that value to something that came from Ziggy. (Right?)

Thanks!

@bakerkretzmar bakerkretzmar merged commit b23a0cb into tighten:2.x Mar 6, 2026
26 checks passed
@pataar
Copy link
Copy Markdown
Contributor Author

pataar commented Mar 6, 2026

Very cool. Re-explaining your code example for my own understanding, and for when I inevitably come back to this to remind myself how it works: now, anything explicitly typed as RouteUrl must come from route(). If I have a variable that's usually a Ziggy-generated route URL but might also be a manually constructed URL string, I have to use string, if I use RouteUrl that means I'm intentionally restricting that value to something that came from Ziggy. (Right?)

Thanks!

Exactly, that way you can validate that your variable must be a validated RouteUrl. This is useful for Link components or custom fetch functions for example.

@pataar
Copy link
Copy Markdown
Contributor Author

pataar commented Mar 6, 2026

Thanks for the merge :)

@pataar pataar deleted the improve/route-url-branded-type branch March 6, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants