Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Params and related functions generic #8019

Merged
merged 3 commits into from
Sep 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reuse the Params type in this file instead of declaring our own Record type here? Not a big deal, just curious.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah totally, will make a TODO for this.

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