Skip to content

Commit

Permalink
Add typescript 4.3 support
Browse files Browse the repository at this point in the history
  • Loading branch information
konowrockis committed Jul 3, 2021
1 parent 4e1e50c commit 4ed4ffb
Show file tree
Hide file tree
Showing 29 changed files with 3,057 additions and 3,957 deletions.
3 changes: 1 addition & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ module.exports = {
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint",
"prettier",
],
parserOptions: {
ecmaVersion: 2018,
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
117 changes: 77 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ const routes = createRouting({
products: segment`/products`,
users: segment`/users/${number('userId')}`,
items: {
...segment`/items${query({ filter: false })}`,
...segment`/items`,
query: {
filter: query()
}
children: {
item: segment`/${uuid('itemId')}`,
},
},
} as const);
});

routes.products(); // '/products'
routes.products.pattern // '/products'
Expand All @@ -51,18 +54,20 @@ routes.items.item.pattern // `/items/:itemId(${uuidRegex})`
To use strongly typed paths, you first have to create the routing object by calling `createRouting` and providing an
object defining segments. Segments represent single routing paths and are implemented as tagged template literals:

```js
```ts
const routes = createRouting({
users: segment`/users`
} as const);
});
```

Second parameter to `createRouting` is `qs` configuration. You can extend/alter `ts-routes` functionality by providing configuration to `qs`. For example you can change array formatting and delimiter. For details on configuration please refer to [`qs` documentation](https://github.com/ljharb/qs).

### Parameters

You can define route params (i.e. parts of the path that are variable) by interpolating the `arg` function inside a
segment:

```js
```ts
segment`/users/${arg("userId")}`;
```

Expand All @@ -72,7 +77,7 @@ By default route parameters are treated as required. You can make them optional
also possible to limit possible parameter values by passing a regex string. While trying to create a route which doesn't
satisfy the pattern, an exception will be thrown.

```js
```ts
segment`/users/${arg("userId", {
optional: true,
pattern: "[0-9]",
Expand All @@ -81,32 +86,36 @@ segment`/users/${arg("userId", {

When creating a route, path parameters can be passed in the first argument:

```js
```ts
routes.users({ userId: "10" });
```

There are some predefined convenience parameter types provided:

- `number(name: string, optional?: boolean)` for number strings
- `uuid(name: string, optional?: boolean)` for UUID strings
- `string(name: string, { optional?: boolean })` for plain strings
- `number(name: string, { optional?: boolean })` for number strings
- `uuid(name: string, { optional?: boolean })` for UUID strings

### Query string

Query string parameters can be specified by interpolating `query` function inside a segment string. The `query` function
Query string parameters can be specified by adding `query` property to the route description. The `query` function
expects an object where keys are names of parameters and values specify whether those params are required in the path.

```js
segment`/product${query({
productId: true,
details: false,
})}`;
```ts
{
...segment`/product`,
query: {
productId: query(false),
details: query(true)
}
}
```

The above segment defines a path which expects the `productId` URL param and the optional `details` URL param.

When creating a route query strings can be passed in the second argument:

```js
```ts
routes.products(
{},
{
Expand All @@ -118,11 +127,58 @@ routes.products(

which will return `/product?details=false&productId=10`.

`qs` by default supports also objects and arrays when stringifying and parsing query string.

```ts
const parameters = routes.products.parseQuery(queryString)

// this will yield given parameters with given type

type Parameters = {
productId: string | string[],
details?: string | string[],
}
```
For objects you need to specify your value type when defining routes:
```ts
import { createRouting, number, query, uuid } from 'ts-routes';

type ProductDetails = { name: string, price: string };

const routes = createRouting({
products: {
...segment`/product`,
query: {
productId: query(false),
details: query<ProductDetails>(true)
}
}
});

const parameters = routes.products.parseQuery(queryString)

// which will yield

type Parameters = {
productId: string | string[],
details?: ProductDetails | ProductDetails[],
}
```
Additionaly you can override `qs` stringify and parse option directly on each route:
```ts
routes.products(undefined, { productId: "10" }, overrideStringifyOptions);

routes.products.parse(queryString, overrideParseOptions);
```

### Nested routes

Routes can be nested by providing an optional `children` property to segments:

```js
```ts
const routes = createRouting({
parent: {
...segment`/parent`,
Expand Down Expand Up @@ -156,7 +212,7 @@ Those patterns are useful for integration with routing libraries which support

You can use patterns for defining routes:

```jsx
```tsx
<Route exact component={ProductsPage} path={routes.products.pattern} />
```

Expand All @@ -165,9 +221,9 @@ With React it's also useful to add some helper types which can be used for typin
```ts
import { FunctionComponent } from "react";
import { RouteComponentProps } from "react-router-dom";
import { RouteParamsFor } from "ts-routes";
import { PathParamsFor } from "ts-routes";

type PageProps<TPathParams extends (...args: any[]) => string> = RouteComponentProps<RouteParamsFor<TPathParams>>;
type PageProps<TPathParams extends (...args: any[]) => string> = RouteComponentProps<PathParamsFor<TPathParams>>;

type PageComponent<TPathParams extends (...args: any[]) => string> = FunctionComponent<PageProps<TPathParams>>;
```
Expand All @@ -184,30 +240,11 @@ const ProductPage: PageComponent<typeof routes.products> = ({
}) => <div>{productId}</div>;
```

And for query string params:
```ts
import { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { QueryParamsFor } from "ts-routes";

function useQueryParams() {
const location = useLocation();
return useMemo(() => new URLSearchParams(location.search), [location.search]);
}

function useQueryParamsGuarded<T extends (...args: any) => any>() {
return useQueryParams() as URLSearchParams & { get(name: keyof NonNullable<QueryParamsFor<T>>): string | null };
}

const redirectUrl = useQueryParamsGuarded<typeof routes.login>().get("redirect");
```
### Vue Router

You can use patterns for defining routes:

```js
```ts
const router = new VueRouter({
routes: [
{
Expand Down
167 changes: 167 additions & 0 deletions __tests__/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { URLSearchParams } from "url";
import { createRouting, number, query, segment } from "../src";

describe("query", () => {
it("creates route with an optional query param", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productId: query(true),
},
},
} as const);

const route = routes.product();

expect(route).toEqual("/product");
});

it("creates route with a required query param", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productId: query(false),
},
},
} as const);

const route = routes.product({}, { productId: "2" });

expect(route).toEqual("/product?productId=2");
});

it("creates route with multiple query params", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productId: query(false),
details: query(true),
},
},
} as const);

const route = routes.product({}, { productId: "2", details: "false" });

expect(route.startsWith(`/product?`));

const searchParams = new URLSearchParams(route.split("?")[1]);
expect(Array.from(searchParams.keys()).length).toEqual(2);
expect(searchParams.get("details")).toEqual("false");
expect(searchParams.get("productId")).toEqual("2");
});

it("adds query params at the end of the path in case of nested routes", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
filter: query(false),
},
children: {
details: segment`/${number("productId")}`,
},
},
} as const);

const route = routes.product.details({ productId: "1" }, { filter: "value" });

expect(route).toEqual(`/product/1?filter=value`);
});

it("ignores query params in the pattern", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productId: query(true),
},
},
} as const);

const pattern = routes.product.pattern;

expect(pattern).toEqual("/product");
});

describe("parsing", () => {
function extractQuery(url: string) {
return url.split("?")[1];
}

it("saves and parses string query properties correctly", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productId: query(),
},
},
});

const url = routes.product(undefined, { productId: "product" });
const parsed = routes.product.parseQuery(extractQuery(url));

expect(parsed.productId).toEqual("product");
});

it("saves and parses arrays correctly", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productsIds: query(),
},
},
});

const url = routes.product(undefined, { productsIds: ["some", "another"] });
const parsed = routes.product.parseQuery(extractQuery(url));

expect(parsed.productsIds).toEqual(["some", "another"]);
});

it("saves and parses objects correctly", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productsIds: query<{ a: string; b: string }>(),
},
},
});

const url = routes.product(undefined, { productsIds: { a: "some", b: "another" } });
const parsed = routes.product.parseQuery(extractQuery(url));

expect(parsed.productsIds).toEqual({ a: "some", b: "another" });
});

it("saves and parses array of objects correctly", () => {
const routes = createRouting({
product: {
...segment`/product`,
query: {
productsIds: query<{ a: string; b: string }>(),
},
},
});

const url = routes.product(undefined, {
productsIds: [
{ a: "some", b: "another" },
{ a: "yet another", b: "and another" },
],
});

const parsed = routes.product.parseQuery(extractQuery(url));

expect(parsed.productsIds).toEqual([
{ a: "some", b: "another" },
{ a: "yet another", b: "and another" },
]);
});
});
});
Loading

0 comments on commit 4ed4ffb

Please sign in to comment.