# Typed routes
## Principles
- Should path-params and search-params be merged?
- PROS merged: simplified mental model
- It is not possible to augment the JSX, this will have to rely on the `route` function. Question is where does the route function come from?
- single global route function generated by the Qwik-City:
- Easy way to remember where to do the import from.
- User must repeat the `path` string, which TS validates / completes
- route function per route written by the developer:
- CONS: this may create issues with bundlers
- CONS: not immediately obvious where it is pointing as the function name may not be consistent.
- Should system provide validation of args? Probably a strong yes, as URL is exposed and people could hack it.
- [name=Ken: Agreed. Types and de/serialization are still a massive help without validation - worth rolling out without validation built in. But it would then be another big win offering validation on top]
- Should system provide a way to do custom serialization / deserialization? => Probably yes, but should it be on per route or global level?
- Global: simpler to maintain and more consistant with global `route` function.
- [name=Ken: Similar to my comment about validation. Big win, not critical. This one is more of a "power user" feature. Global for sure. Maybe global + per-route overrides?]
### Misko's general thoughts
- Each route should declare `RouteParams` data type.
- A well known exported type.
- I am concerned that putting the helper function into the route will create bundling issues. I don't think bundlers will be able to disentangle it, and I am concerned that referring to the route will bring the route file in.
- I am leaning towards that the route exports type only
- Referring to the route is done through string that way no bundling surprises.
- [name=Ken: I'm very much on the same page with all of this. Single route() function that you import is both simple and easy]
- We should have a top level `url-encoding.ts` file which developer can optionally create that can override the default encoding/decoding of the `RouteParams`
- [name=Ken: Beautiful. Top level config makes perfect sense here]
-
## Constraints
- REQ-RTTI: must have runtime types so that it can verify that the inputs are correct. (URLs can be edited and hence we can't rely on design time validation only.)
- [name=Ken: I view the need for validation not as a constraint but as an additional feature to provide. Validation is more than just about type (string is at least X chars, number is greater than some number, etc). Also forgive me if this is what you meant; I'm not sure what RTTI stands for]
- REQ-INHERIT: Given `/[foo]/` and `/[foo]/[bar]` system must ensure that `foo` is of same type and serialization in both routes.
- [name=Ken: This provides another case for path params just being typed as strings]
## Prototypes
- Misko: [defineRoute](https://stackblitz.com/edit/qwik-starter-7my1vk)
- CONCERN: because `defineRoute` is in the route file worried that the bundler will not know how to break up the files correctly.
- CON: same import `~/code-gen` will have a different output depending where it is imported from.
- CON: `RouteParams` must be exported so that child routes can correctly maintain type
- OPEN ISSUE: How will the child routes work with middleware so that the same information can be on many levels.
- Dustin: [link](https://stackblitz.com/edit/qwik-starter-m5qpt2)
## Exporting Route `Params`
```typescript!
export interface RouteParams {
contactId: string;
predicate: {
name: string
age: number
};
}
```
### Comments
Wout:
- I don't understand `predicate` here.
- I suppose Qwik-City will be auto-generating the routes interface? E.g. at `/routes/routes.d.ts`?
- How would Qwik-City know what the type of param should be? That can then also be checked at runtime.
> [name=Miško Hevery] In the design which I presented in the meeting it would not know. Since then I have come to the conclusion that we must have types at runtime as well.
- when desired, you can add to the interface via a separate .d.ts file, for example when making a complex `[...rest]` route
- Check out [io-ts](https://github.com/gcanti/io-ts/blob/master/index.md), it enables runtime type checking + encoding (:thinking_face: might be good for making tsurl2)
- How to type `[...rest]` route?
> [name=Miško Hevery] it should just be normal type as in `interface RouteParams { rest: string }`
- How about also typing the query?
> [name=Miško Hevery] any param which is not in the path would end up in the Query. So the same `RouteParams` can be used to type both.
## Creating URLs in TypeSafe manner
Options 1: `toUrl` function:
```typescript!
export const Link: {
(params: __ALL_ROUTES): ReturnType<typeof UntypedLink>;
/**
* No `RouteParams` provided
*/
toUrl(
route: "/contacts/",
params?: {} // NOT DECLARED ASSUMED NO PARAMS
): string;
/**
* {@link 〳contacts〳ᐸcontactIDᑀ〳ːRouteParams}
*/
toUrl(
route: "/contacts/[contactID]/",
params: 〳contacts〳ᐸcontactIDᑀ〳ːRouteParams
): string;
/**
* {@link 〳contacts〳ᐸcontactIDᑀ〳delete〳ːRouteParams}
*/
toUrl(
route: "/contacts/[contactID]/delete/",
params: 〳contacts〳ᐸcontactIDᑀ〳delete〳ːRouteParams
): string;
} = UntypedLink as any;
Link.toUrl = encodeParamsToUrl;
```
Option 2: Factory function
```typescript!
import {ROUTE_PATH}
export const contactRoute = defineRoute<RouteParams>(ROUTE_PATH);
```
> Ken:
I like the toUrl approach (could be in Link but also in useNavigate for further extensibility). The typegen I think would end up pretty simple. A file that ends up looking something like:
> [name=Miško Hevery] I like that approach too. It seams less magic than [defineRoute](https://stackblitz.com/edit/qwik-starter-7my1vk) but does not have a good solution for runtime type information and validation.
```typescript!
//This is an automatically generated file. There is no need to update it manually. Manual updates will be overridden
import type { RouteParams as params0 } from "./routes/profile/[contact]"
import type { RouteParams as params1 } from "./routes/product/[productId]"
export type ParamsByRoute = {
"/": null;
"/flower": null;
"/profile/[contact]": params0;
"/product/[productId]": params1;
"/article/[articleId]": string;
}
```
Where the `null` are those without route params at all, the `string` are where there are params given the structure of the directory, but no RouteParams interface was exported, and the rest are the imported RouteParams;
Then the toUrl function can be typed like
```typescript!
function toUrl<Route extends keyof ParamsByRoute>(
...args: ParamsByRoute[Route] extends null ?
[route: Route]
: [route: Route, params: ParamsByRoute[Route]]
) {
```
### Type system in action:

The above is with the following RouteParams exported under "/profile/[contact]":
```typescript!
export interface RouteParams {
id: number;
name: string;
tags: string[];
}
```
and the following was the RouteParams for "/product/[productId]":
```typescript!
export interface RouteParams {
id: string;
}
```
## Types other than strings
When using encoding like jsurl2, you can encode the type in the param. Params `"hello"`, `123`, `true`, `"123true"` would be encoded as `/route/hello/123/_T/*123true`.
So then the RouteParams interface can include the type of the param, but of course it needs runtime verification.
> [name=Misko] I like this approach, but we need to solve few more things:
> - REQ-RTTI: we need to ensure that we have the type information at runtime so that we can correctly verify types of URLs
> - REQ-INHERIT: We need to enuser that child routes don't change the types.
---
# HIGH-LEVEL DESIGN DOCUMENT
---
NOTE: This is a high-level design, many parts are left unanswered.
## Goals:
- Routes should be type-safe. A change in route should throw a TS error.
- IMPLICATION: Some function needs to exist to verify that the route is correct. (Not possible to augment the `<a href>` type.) The resulting syntax will be `<a href={someFn(...)}/>`.
- Route params should by type-safe as well.
- IMPLICATION: The developer needs to declare the types in tho route file. When crating a route, the TS should verify that the types match. `<a href={someFn(..., {paramA: "valueA", ...})}/>`
- Route serialization/deserialization should be configurable.
- IMPLICATION: A reasonable default should be provided but the developer should be able to override the serializer/deserializer.
- Route params should be verifiable, because the URL is easily hackable by the user and we can't rely on design-type types alone.
## File Layout
```
src
+- components
+- routes
+- config
+- routes-types.d.ts // AUTO-GENERATED
+- route.ts // PRE-GENERATED by npm create
+- route-link.ts // but developer can change the
+- url-decode.ts // implementation
```
### `routes-types.d.ts`
This file will be generated by the QwikCity build system. Notice that the file is `.d.ts` which means it can contain type information only. This is intentional as for now I don't think we should go to the business of code-generation.
A pseudo implementation of the file:
```typescript!
import type { RouteParams as 〳contacts〳ᐸcontactIDᑀ〳ːRouteParams } from "./contacts/[contactID]";
import type { RouteParams as 〳contacts〳ᐸcontactIDᑀ〳delete〳ːRouteParams } from "./contacts/[contactID]/delete";
export type 〳contacts〳 = Route<
"/contacts/",
never,
{} // NOT DECLARED ASSUME NO PARAMS
>;
export type 〳contacts〳ᐸcontactIDᑀ〳 = Route<
"/contacts/[contactID]/",
"contactID",
〳contacts〳ᐸcontactIDᑀ〳ːRouteParams,
SlugParams<〳contacts〳>
>;
export type 〳contacts〳ᐸcontactIDᑀ〳delete〳 = Route<
"/contacts/[contactID]/delete/",
"contactID",
// DO YOU SEE TYPESCRIPT ERROR ON ABOVE LINE?
// If so, then the issue is at: FILE:line:col
〳contacts〳ᐸcontactIDᑀ〳delete〳ːRouteParams,
// DO YOU SEE TYPESCRIPT ERROR ON ABOVE LINE?
// If so, then the issue is at: FILE:line:col
SlugParams<〳contacts〳ᐸcontactIDᑀ〳>
>;
export Routes =
| LinkFromRoute<〳contacts〳>
| LinkFromRoute<〳contacts〳ᐸcontactIDᑀ〳>
| LinkFromRoute<〳contacts〳ᐸcontactIDᑀ〳delete〳>;
```
### `routes.ts`
This file will be created by the Qwik starter and will contain configuration for the URL serializer/deserializer etc.. A default file will be created but it is up to developer to customize it. The file will have well known exports which the rest of the system can use.
A pseudo implementation of the file:
```typescript!
import {
Link,
URLDeserializer,
decodeParamsFromUrl,
encodeParamsToUrl,
} from '@builder.io/qwik-city';
import {Routes} from './routes-types';
// Well known export which is used by the QwikCity to automatically deserialize requests
export const urlDeserializer: URLDeserializer = (request) => {
return decodeParamsFromUrl(request.path, request.search);
};
// Well known export which is used developer to build Routes.
export const RouteLink: (params: Routes): ReturnType<typeof DefaultLink> = Link;
export const route: (path: Path<Routes>, params: Params<Routes>): string = encodeParamsToUrl;
```
### `/contacts/[contactID]/`
An example route:
```typescript!
// Well known export to be used by `routes-types.d.ts`
export interface RouteParams {
contactID: string;
predicate?: {
age?: number
}
}
export const onGet: RequestHandler<RouteParams, EndpointResponse> = ({
params,
}) => {
// 1. params are parsed by the `urlDeserializer` function in `routes.ts`
// 2. params here are of `RouteParams` type
console.log(params);
};
```
### Usage
```typescript!
import {RouteLink, route} from './routes';
return (
<>
<a href="http://you-can-always-do-the-unsafe-thing">link</a>
<a href={route('/contacts/[contactID]', {
'[contactID]': 'abc',
predicate: {age: 30}
})}>link</a>
<RouteLink href={route('/contacts/[contactID]', {
'[contactID]': 'abc',
predicate: {age: 30}
})}>link</RouteLink>
<RouteLink href="/contacts/[contactID]" contactID='abc' predicate={{age:30}}>
text
</RouteLink>
<RouteLink href="/contacts/[contactID]" contactID='abc' predicate={{age:30}}>
text (kind of broken)
</RouteLink>
<RouteLink link:href="/contacts/[contactID]" contactID='abc' predicate={{age:30}}>
text
</RouteLink>
<RouteLink href="/contacts/[contactID]" params={{
'[contactID]': 'abc',
predicate: {age: 30}
}}>
text
</RouteLink>
</>
);
```
## Verification of Params
```typescript!
export interface RouteParams {
contactID: string;
predicate?: {
age?: number
}
}
// Well known export
export onGetRouteParamsAssertion: TypeAssertion<RouteParams>: {
contactID: {
required: true,
type: String,
assert: (v) => {
if (v.length < 3) throw new AssertError('key to short');
}
},
predicate: {
required: false,
properties: {
age: {
defaultValue: () => 21;
type: Number;
assert: (v) => {
if (v < 21) throw new Error('Underaged')
}
}
}
}
}
export const onGetCatch: CatchHandler<...> = ...;
export const onGet: RequestHandler<RouteParams, EndpointResponse> = ({
params,
}) => {
// If `routeParamsAssertion` is present then it will be called automatically
// Equivalent to the call below:
// assertObjectShape(params, routeParamsAssertion);
console.log(params);
};
export default component$<RouteParams>((params) => {
// assertObjectShape(params, routeParamsAssertion);
});
```