diff --git a/README.md b/README.md index fe1b4ad..a3be19e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ yarn add ts-routes ## Quick start -```js +```ts import { createRouting, number, query, segment, uuid } from 'ts-routes'; const routes = createRouting({ diff --git a/__tests__/query.test.ts b/__tests__/query.test.ts index b4c1296..a1f51f7 100644 --- a/__tests__/query.test.ts +++ b/__tests__/query.test.ts @@ -7,7 +7,7 @@ describe("query", () => { product: { ...segment`/product`, query: { - productId: query(true), + productId: query("optional"), }, }, } as const); @@ -22,7 +22,7 @@ describe("query", () => { product: { ...segment`/product`, query: { - productId: query(false), + productId: query("required"), }, }, } as const); @@ -37,8 +37,8 @@ describe("query", () => { product: { ...segment`/product`, query: { - productId: query(false), - details: query(true), + productId: query("required"), + details: query("optional"), }, }, } as const); @@ -58,7 +58,7 @@ describe("query", () => { product: { ...segment`/product`, query: { - filter: query(false), + filter: query("required"), }, children: { details: segment`/${number("productId")}`, @@ -76,7 +76,7 @@ describe("query", () => { product: { ...segment`/product`, query: { - productId: query(true), + productId: query("optional"), }, }, } as const); diff --git a/__tests__/segments/arg.test.ts b/__tests__/segments/arg.test.ts index b77b23b..5c5d4da 100644 --- a/__tests__/segments/arg.test.ts +++ b/__tests__/segments/arg.test.ts @@ -1,28 +1,6 @@ -import { string, createRouting, segment, arg } from "../../src"; +import { createRouting, segment, arg } from "../../src"; describe("arg segment", () => { - it("creates route with an arg segment", () => { - const routes = createRouting({ - product: segment`/product/${string("productId")}`, - } as const); - - const route = routes.product({ productId: "id" }); - - expect(route).toEqual("/product/id"); - }); - - it("creates route with an optional arg segment", () => { - const routes = createRouting({ - product: segment`/product/${string("productId", { - optional: true, - })}`, - } as const); - - const route = routes.product(); - - expect(route).toEqual("/product"); - }); - it("creates route with a custom pattern param", () => { const routes = createRouting({ product: segment`/product/${arg("productId", { @@ -45,28 +23,6 @@ describe("arg segment", () => { expect(() => routes.product({ productId: "123" })).toThrow(); }); - it("returns the correct path pattern when required", () => { - const routes = createRouting({ - product: segment`/product/${string("productId")}`, - } as const); - - const pattern = routes.product.pattern; - - expect(pattern).toEqual("/product/:productId"); - }); - - it("returns the correct path pattern when optional", () => { - const routes = createRouting({ - product: segment`/product/${string("productId", { - optional: true, - })}`, - } as const); - - const pattern = routes.product.pattern; - - expect(pattern).toEqual("/product/:productId?"); - }); - it("returns the correct path pattern for custom regexes", () => { const routes = createRouting({ product: segment`/product/${arg("productId", { diff --git a/__tests__/segments/number.test.ts b/__tests__/segments/number.test.ts index 651eef4..977f5a7 100644 --- a/__tests__/segments/number.test.ts +++ b/__tests__/segments/number.test.ts @@ -3,7 +3,7 @@ import { createRouting, number, segment } from "../../src"; describe("number segment", () => { it("creates route with an optional number param", () => { const routes = createRouting({ - product: segment`/product/${number("productId", { optional: true })}`, + product: segment`/product/${number("productId", "optional")}`, } as const); const route = routes.product(); @@ -41,7 +41,7 @@ describe("number segment", () => { it("returns the correct pattern when optional", () => { const routes = createRouting({ - product: segment`/product/${number("productId", { optional: true })}`, + product: segment`/product/${number("productId", "optional")}`, } as const); const pattern = routes.product.pattern; diff --git a/__tests__/segments/string.test.ts b/__tests__/segments/string.test.ts new file mode 100644 index 0000000..5649560 --- /dev/null +++ b/__tests__/segments/string.test.ts @@ -0,0 +1,43 @@ +import { string, createRouting, segment } from "../../src"; + +describe("arg segment", () => { + it("creates route with an arg segment", () => { + const routes = createRouting({ + product: segment`/product/${string("productId")}`, + } as const); + + const route = routes.product({ productId: "id" }); + + expect(route).toEqual("/product/id"); + }); + + it("creates route with an optional arg segment", () => { + const routes = createRouting({ + product: segment`/product/${string("productId", "optional")}`, + } as const); + + const route = routes.product(); + + expect(route).toEqual("/product"); + }); + + it("returns the correct path pattern when required", () => { + const routes = createRouting({ + product: segment`/product/${string("productId")}`, + } as const); + + const pattern = routes.product.pattern; + + expect(pattern).toEqual("/product/:productId"); + }); + + it("returns the correct path pattern when optional", () => { + const routes = createRouting({ + product: segment`/product/${string("productId", "optional")}`, + } as const); + + const pattern = routes.product.pattern; + + expect(pattern).toEqual("/product/:productId?"); + }); +}); diff --git a/__tests__/segments/uuid.test.ts b/__tests__/segments/uuid.test.ts index 15d0e0e..a0a7f86 100644 --- a/__tests__/segments/uuid.test.ts +++ b/__tests__/segments/uuid.test.ts @@ -5,7 +5,7 @@ describe("uuid segment", () => { it("creates route with an optional uuid param", () => { const routes = createRouting({ - product: segment`/product/${uuid("productId", { optional: true })}`, + product: segment`/product/${uuid("productId", "optional")}`, } as const); const route = routes.product(); diff --git a/__tests__/types/index.test-d.ts b/__tests__/types/index.test-d.ts index ee90797..36e3f00 100644 --- a/__tests__/types/index.test-d.ts +++ b/__tests__/types/index.test-d.ts @@ -7,15 +7,15 @@ const routes = createRouting({ products: { ...segment`/products`, query: { - filter: query(false), - optionalFilter: query(true), - object: query<{ test: string }, true>(true), + filter: query("required"), + optionalFilter: query("optional"), + object: query<{ test: string }, "optional">("optional"), }, children: { product: segment`/${number("productId")}`, }, }, - order: segment`/orders/${number("orderId", { optional: true })}`, + order: segment`/orders/${number("orderId", "optional")}`, }); // Should not allow creating routing from raw strings diff --git a/package.json b/package.json index 330cf36..084d802 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-routes", - "version": "2.0.0", + "version": "2.0.0-alpha.0", "description": "Strongly typed routes management", "main": "lib/index.cjs.js", "module": "lib/index.esm.js", diff --git a/src/PathParamDescription.ts b/src/PathParamDescription.ts index 98fcda0..4ff9fa2 100644 --- a/src/PathParamDescription.ts +++ b/src/PathParamDescription.ts @@ -1,14 +1,19 @@ -export default class PathParamDescription { +import { Optionality } from "./helpers"; + +export default class PathParamDescription< + TName extends string = string, + TOptionality extends Optionality = "optional", +> { public readonly pattern: string; public readonly name: TName; - public readonly optional: TOptional; + public readonly optionality: TOptionality; - constructor({ name, optional, pattern }: { name: TName; optional: TOptional; pattern?: string }) { + constructor({ name, optionality, pattern }: { name: TName; optionality: TOptionality; pattern?: string }) { const patternPart = pattern ? `(${pattern})` : ""; - const requirementPart = optional ? "?" : ""; + const requirementPart = optionality === "optional" ? "?" : ""; this.name = name; - this.optional = optional ?? (false as any); + this.optionality = optionality; this.pattern = `:${name}${patternPart}${requirementPart}`; } } diff --git a/src/QueryParamDescription.ts b/src/QueryParamDescription.ts index b8f4f0c..081487b 100644 --- a/src/QueryParamDescription.ts +++ b/src/QueryParamDescription.ts @@ -1,5 +1,7 @@ -export default class QueryParamDescription { +import { Optionality } from "./helpers"; + +export default class QueryParamDescription { private readonly _?: TReturnType; - constructor(public readonly optional: TOptional) {} + constructor(public readonly optionality: TOptionality) {} } diff --git a/src/RouteDescription.ts b/src/RouteDescription.ts index 6caf096..3325bc6 100644 --- a/src/RouteDescription.ts +++ b/src/RouteDescription.ts @@ -1,10 +1,11 @@ import SegmentPattern from "./SegmentPattern"; import QueryParamDescription from "./QueryParamDescription"; import PathParamDescription from "./PathParamDescription"; +import { Optionality } from "./helpers"; export default interface RouteDescription< - TPathParams extends PathParamDescription[] = [], - TQueryParams extends Record> = {}, + TPathParams extends PathParamDescription[] = [], + TQueryParams extends Record> = {}, TChildren extends { readonly [name: string]: RouteDescription } = {}, > { readonly pattern: SegmentPattern; diff --git a/src/SegmentPattern.ts b/src/SegmentPattern.ts index 12e2398..c84dfd4 100644 --- a/src/SegmentPattern.ts +++ b/src/SegmentPattern.ts @@ -1,5 +1,6 @@ +import { Optionality } from "./helpers"; import PathParamDescription from "./PathParamDescription"; -export default class SegmentPattern[]> { +export default class SegmentPattern[]> { constructor(public readonly pattern: string, public readonly params: TPathParamsDescription) {} } diff --git a/src/createRouting.ts b/src/createRouting.ts index bd11c2f..62c08c4 100644 --- a/src/createRouting.ts +++ b/src/createRouting.ts @@ -1,5 +1,6 @@ import { compile } from "path-to-regexp"; import { IParseOptions, IStringifyOptions, parse, stringify } from "qs"; +import { Optionality } from "./helpers"; import PathParamDescription from "./PathParamDescription"; import QueryParamDescription from "./QueryParamDescription"; import RouteDescription from "./RouteDescription"; @@ -53,7 +54,7 @@ type GetQueryParams> = type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never; -type MapPathParams[]> = UnionToIntersection< +type MapPathParams[]> = UnionToIntersection< { [T in keyof TPathParams]: GetParam; }[number] @@ -61,22 +62,22 @@ type MapPathParams[]> type SingleOrArray = T | T[]; -type MapQueryParams>> = { - [TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription +type MapQueryParams>> = { + [TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription ? TName : never]?: SingleOrArray>; } & { - [TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription + [TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription ? TName : never]: SingleOrArray>; }; -type GetQueryResultType> = - TQueryParam extends QueryParamDescription ? TReturnType : never; +type GetQueryResultType> = + TQueryParam extends QueryParamDescription ? TReturnType : never; type GetParam = TPathParam extends PathParamDescription - ? TOptional extends true + ? TOptional extends "optional" ? { [T in TName]?: string; } diff --git a/src/helpers.ts b/src/helpers.ts index 126ec7b..8c22f3c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -3,3 +3,5 @@ export type PathParamsFor string> = Parameters[ : NonNullable[0]>; export type QueryParamsFor string> = Parameters[1]; + +export type Optionality = "required" | "optional"; diff --git a/src/parameters/arg.ts b/src/parameters/arg.ts index d1fdb0a..f449238 100644 --- a/src/parameters/arg.ts +++ b/src/parameters/arg.ts @@ -1,14 +1,15 @@ +import { Optionality } from "../helpers"; import PathParamDescription from "../PathParamDescription"; -export default function arg( +export default function arg( name: TName, { pattern, - optional = false as TOptional, + optionality = "required" as TOptionality, }: { pattern?: string; - optional?: TOptional; + optionality?: TOptionality; } = {}, ) { - return new PathParamDescription({ name, optional, pattern }); + return new PathParamDescription({ name, optionality, pattern }); } diff --git a/src/parameters/number.ts b/src/parameters/number.ts index 29f6d93..9472cbf 100644 --- a/src/parameters/number.ts +++ b/src/parameters/number.ts @@ -1,14 +1,22 @@ +import { Optionality } from "../helpers"; import PathParamDescription from "../PathParamDescription"; -export default function uuid( +export default function uuid( name: TName, - { - optional = false as TOptional, - }: { - optional?: TOptional; - } = {}, + optsOrOptionality?: + | { + optionality?: TOptionality; + } + | TOptionality, ) { + let optionality: TOptionality; + if (typeof optsOrOptionality === "string") { + optionality = optsOrOptionality; + } else { + optionality = optsOrOptionality?.optionality ?? ("required" as TOptionality); + } + const number = "[0-9]+"; - return new PathParamDescription({ name, optional, pattern: number }); + return new PathParamDescription({ name, optionality, pattern: number }); } diff --git a/src/parameters/string.ts b/src/parameters/string.ts index 53bca43..ce8287d 100644 --- a/src/parameters/string.ts +++ b/src/parameters/string.ts @@ -1,12 +1,20 @@ +import { Optionality } from "../helpers"; import PathParamDescription from "../PathParamDescription"; -export default function string( +export default function string( name: TName, - { - optional = false as TOptional, - }: { - optional?: TOptional; - } = {}, + optsOrOptionality?: + | { + optionality?: TOptionality; + } + | TOptionality, ) { - return new PathParamDescription({ name, optional }); + let optionality: TOptionality; + if (typeof optsOrOptionality === "string") { + optionality = optsOrOptionality; + } else { + optionality = optsOrOptionality?.optionality ?? ("required" as TOptionality); + } + + return new PathParamDescription({ name, optionality }); } diff --git a/src/parameters/uuid.ts b/src/parameters/uuid.ts index 5ffa492..c9b3fc8 100644 --- a/src/parameters/uuid.ts +++ b/src/parameters/uuid.ts @@ -1,14 +1,22 @@ +import { Optionality } from "../helpers"; import PathParamDescription from "../PathParamDescription"; -export default function uuid( +export default function uuid( name: TName, - { - optional = false as TOptional, - }: { - optional?: TOptional; - } = {}, + optsOrOptionality?: + | { + optionality?: TOptionality; + } + | TOptionality, ) { + let optionality: TOptionality; + if (typeof optsOrOptionality === "string") { + optionality = optsOrOptionality; + } else { + optionality = optsOrOptionality?.optionality ?? ("required" as TOptionality); + } + const uuid = "[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}"; - return new PathParamDescription({ name, optional, pattern: uuid }); + return new PathParamDescription({ name, optionality, pattern: uuid }); } diff --git a/src/query.ts b/src/query.ts index 98ed07b..b72b683 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,9 +1,11 @@ +import { Optionality } from "./helpers"; import QueryParamDescription from "./QueryParamDescription"; type AllowedQueryParams = string | { [key: string]: AllowedQueryParams }; -export default function query( - optional: TOptional = false as TOptional, -) { - return new QueryParamDescription(optional); +export default function query< + TReturnType extends AllowedQueryParams = string, + TOptionality extends Optionality = "required", +>(optionality: TOptionality = "required" as TOptionality) { + return new QueryParamDescription(optionality); } diff --git a/src/segment.ts b/src/segment.ts index 3e19578..833690a 100644 --- a/src/segment.ts +++ b/src/segment.ts @@ -1,7 +1,8 @@ +import { Optionality } from "./helpers"; import PathParamDescription from "./PathParamDescription"; import SegmentPattern from "./SegmentPattern"; -export default function segment[]>( +export default function segment[]>( literals: TemplateStringsArray, ...placeholders: TPathParamsDescription ) {