Skip to content

Commit

Permalink
Make Params and related functions generic (#8019)
Browse files Browse the repository at this point in the history
* Add generic for params type

* Add undefined for param type

* fix test types
  • Loading branch information
chaance committed Sep 9, 2021
1 parent 2e3c14c commit 5acd319
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 34 deletions.
12 changes: 7 additions & 5 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ See also [`createRoutesFromArray`](#createroutesfromarray).
<summary>Type declaration</summary>

```tsx
declare function generatePath(path: string, params: Params = {}): string;
declare function generatePath(path: string, params?: Params): string;
```

</details>
Expand Down Expand Up @@ -813,10 +813,10 @@ This is the heart of React Router's matching algorithm. It is used internally by
<summary>Type declaration</summary>

```tsx
declare function matchPath(
declare function matchPath<ParamKey extends string = string>(
pattern: PathPattern,
pathname: string
): PathMatch | null;
): PathMatch<ParamKey> | null;

type PathPattern =
| string
Expand Down Expand Up @@ -1046,7 +1046,9 @@ function App() {
<summary>Type declaration</summary>

```tsx
declare function useMatch(pattern: PathPattern): PathMatch | null;
declare function useMatch<ParamKey extends string = string>(
pattern: PathPattern
): PathMatch<ParamKey> | null;
```

</details>
Expand Down Expand Up @@ -1119,7 +1121,7 @@ Returns the element for the child route at this level of the route hierarchy. Th
<summary>Type declaration</summary>

```tsx
declare function useParams(): Params;
declare function useParams<Key extends string = string>(): Params<Key>;
```

</details>
Expand Down
16 changes: 8 additions & 8 deletions packages/react-router/__tests__/useParams-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
describe("useParams", () => {
describe("when the route isn't matched", () => {
it("returns an empty object", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function Home() {
params = useParams();
return null;
Expand All @@ -30,7 +30,7 @@ describe("useParams", () => {

describe("when the path has no params", () => {
it("returns an empty object", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function Home() {
params = useParams();
return null;
Expand All @@ -51,7 +51,7 @@ describe("useParams", () => {

describe("when the path has some params", () => {
it("returns an object of the URL params", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function BlogPost() {
params = useParams();
return null;
Expand All @@ -73,7 +73,7 @@ describe("useParams", () => {

describe("a child route", () => {
it("returns a combined hash of the parent and child params", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};

function Course() {
params = useParams();
Expand Down Expand Up @@ -110,7 +110,7 @@ describe("useParams", () => {

describe("when the path has percent-encoded params", () => {
it("returns an object of the decoded params", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function BlogPost() {
params = useParams();
return null;
Expand All @@ -133,7 +133,7 @@ describe("useParams", () => {

describe("when the path has a + character", () => {
it("returns an object of the decoded params", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function BlogPost() {
params = useParams();
return null;
Expand Down Expand Up @@ -169,7 +169,7 @@ describe("useParams", () => {
});

it("returns the raw value and warns", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function BlogPost() {
params = useParams();
return null;
Expand All @@ -196,7 +196,7 @@ describe("useParams", () => {

describe("when the params match in a child route", () => {
it("renders params in the parent", () => {
let params: Record<string, string> = {};
let params: Record<string, string | undefined> = {};
function Blog() {
params = useParams();
return <h1>{params.slug}</h1>;
Expand Down
56 changes: 35 additions & 21 deletions packages/react-router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import type {
Transition
} from "history";

type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

const readOnly: <T>(obj: T) => Readonly<T> = __DEV__
? obj => Object.freeze(obj)
: obj => obj;
Expand Down Expand Up @@ -87,9 +91,9 @@ const RouteContext = React.createContext<RouteContextObject>({
route: null
});

interface RouteContextObject {
interface RouteContextObject<ParamKey extends string = string> {
outlet: React.ReactElement | null;
params: Params;
params: Params<ParamKey>;
pathname: string;
basename: string;
route: RouteObject | null;
Expand Down Expand Up @@ -374,7 +378,9 @@ export function useLocation(): Location {
*
* @see https://reactrouter.com/api/useMatch
*/
export function useMatch(pattern: PathPattern): PathMatch | null {
export function useMatch<ParamKey extends string = string>(
pattern: PathPattern
): PathMatch<ParamKey> | null {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
Expand All @@ -383,7 +389,7 @@ export function useMatch(pattern: PathPattern): PathMatch | null {
);

let location = useLocation() as Location;
return matchPath(pattern, location.pathname);
return matchPath<ParamKey>(pattern, location.pathname);
}

type PathPattern =
Expand Down Expand Up @@ -468,7 +474,7 @@ export function useOutlet(): React.ReactElement | null {
*
* @see https://reactrouter.com/api/useParams
*/
export function useParams(): Params {
export function useParams<Key extends string = string>(): Params<Key> {
return React.useContext(RouteContext).params;
}

Expand Down Expand Up @@ -666,7 +672,9 @@ export function createRoutesFromChildren(
/**
* The parameters that were parsed from the URL path.
*/
export type Params = Record<string, string>;
export type Params<Key extends string = string> = {
readonly [key in Key]: string | undefined;
};

/**
* A route object represents a logical route, with (optionally) its child
Expand Down Expand Up @@ -700,7 +708,7 @@ export function generatePath(path: string, params: Params = {}): string {
return path
.replace(/:(\w+)/g, (_, key) => {
invariant(params[key] != null, `Missing ":${key}" param`);
return params[key];
return params[key]!;
})
.replace(/\/*\*$/, _ =>
params["*"] == null ? "" : params["*"].replace(/^\/*/, "/")
Expand Down Expand Up @@ -750,10 +758,10 @@ export function matchRoutes(
return matches;
}

export interface RouteMatch {
export interface RouteMatch<ParamKey extends string = string> {
route: RouteObject;
pathname: string;
params: Params;
params: Params<ParamKey>;
}

function flattenRoutes(
Expand Down Expand Up @@ -861,13 +869,13 @@ function stableSort(array: any[], compareItems: (a: any, b: any) => number) {
array.sort((a, b) => compareItems(a, b) || copy.indexOf(a) - copy.indexOf(b));
}

function matchRouteBranch(
function matchRouteBranch<ParamKey extends string = string>(
branch: RouteBranch,
pathname: string
): RouteMatch[] | null {
): RouteMatch<ParamKey>[] | null {
let routes = branch[1];
let matchedPathname = "/";
let matchedParams: Params = {};
let matchedParams = {} as Params<ParamKey>;

let matches: RouteMatch[] = [];
for (let i = 0; i < routes.length; ++i) {
Expand All @@ -893,7 +901,7 @@ function matchRouteBranch(
matches.push({
route,
pathname: matchedPathname,
params: readOnly<Params>(matchedParams)
params: readOnly<Params<ParamKey>>(matchedParams)
});
}

Expand All @@ -906,10 +914,10 @@ function matchRouteBranch(
*
* @see https://reactrouter.com/api/matchPath
*/
export function matchPath(
export function matchPath<ParamKey extends string = string>(
pattern: PathPattern,
pathname: string
): PathMatch | null {
): PathMatch<ParamKey> | null {
if (typeof pattern === "string") {
pattern = { path: pattern };
}
Expand All @@ -922,18 +930,24 @@ export function matchPath(

let matchedPathname = match[1];
let values = match.slice(2);
let params = paramNames.reduce((memo, paramName, index) => {
memo[paramName] = safelyDecodeURIComponent(values[index] || "", paramName);
return memo;
}, {} as Params);
let params: Params = paramNames.reduce<Mutable<Params>>(
(memo, paramName, index) => {
memo[paramName] = safelyDecodeURIComponent(
values[index] || "",
paramName
);
return memo;
},
{}
);

return { path, pathname: matchedPathname, params };
}

export interface PathMatch {
export interface PathMatch<ParamKey extends string = string> {
path: string;
pathname: string;
params: Params;
params: Params<ParamKey>;
}

function compilePath(
Expand Down

0 comments on commit 5acd319

Please sign in to comment.