Skip to content

Commit

Permalink
feat: MetaFunction type infers data and parentsData types from …
Browse files Browse the repository at this point in the history
…loaders (#4022)

* feat: metafunction type can now infer `data` and `parentsData` types from loaders

* Create stupid-houses-sing.md

* docs(conventions): explanation and example for `MetaFunction` type inference
  • Loading branch information
pcattori authored Aug 19, 2022
1 parent df57153 commit f793b61
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 5 deletions.
52 changes: 52 additions & 0 deletions .changeset/stupid-houses-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
"remix": minor
"@remix-run/serve": minor
"@remix-run/server-runtime": minor
---

`MetaFunction` type can now infer `data` and `parentsData` types from loaders

For example, if this meta function is for `/sales/customers/$customerId`:

```ts
// app/root.tsx
const loader = () => {
return json({ hello: "world" } as const)
}
export type Loader = typeof loader

// app/routes/sales.tsx
const loader = () => {
return json({ salesCount: 1074 })
}
export type Loader = typeof loader

// app/routes/sales/customers.tsx
const loader = () => {
return json({ customerCount: 74 })
}
export type Loader = typeof loader

// app/routes/sales/customers/$customersId.tsx
import type { Loader as RootLoader } from "../../../root"
import type { Loader as SalesLoader } from "../../sales"
import type { Loader as CustomersLoader } from "../../sales/customers"

const loader = () => {
return json({ name: "Customer name" })
}

const meta: MetaFunction<typeof loader, {
"root": RootLoader
"routes/sales": SalesLoader,
"routes/sales/customers": CustomersLoader,
}> = ({ data, parentsData }) => {
const { name } = data
// ^? string
const { customerCount } = parentsData["routes/sales/customers"]
// ^? number
const { salesCount } = parentsData["routes/sales"]
// ^? number
const { hello } = parentsData["root"]
// ^? "world"
}
32 changes: 30 additions & 2 deletions docs/api/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1072,22 +1072,50 @@ export const meta: MetaFunction = () => ({
- `parentsData` is a hashmap of all the data exported by `loader` functions of current route and all of its parents

```tsx
export const meta: MetaFunction = ({ data, params }) => {
export const meta: MetaFunction<typeof loader> = ({ data, params }) => {
if (!data) {
return {
title: "Missing Shake",
description: `There is no shake with the ID of ${params.shakeId}. 😢`,
};
}

const { shake } = data as LoaderData;
const { shake } = data;
return {
title: `${shake.name} milkshake`,
description: shake.summary,
};
};
```

To infer types for `parentsData`, provide a mapping from the route's file path (relative to `app/`) to that route loader type:

```tsx
// app/routes/sales.tsx
const loader = () => {
return json({ salesCount: 1074 })
}
export type Loader = typeof loader

```

```tsx
import type { Loader as SalesLoader } from "../../sales"

const loader = () => {
return json({ name: "Customer name" })
}

const meta: MetaFunction<typeof loader, {
"routes/sales": SalesLoader,
}> = ({ data, parentsData }) => {
const { name } = data
// ^? string
const { salesCount } = parentsData["routes/sales"]
// ^? number
}
```

### `links`

The links function defines which `<link>` elements to add to the page when the user visits a route.
Expand Down
63 changes: 60 additions & 3 deletions packages/remix-server-runtime/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Params } from "react-router-dom";
import type { AppLoadContext, AppData } from "./data";
import type { LinkDescriptor } from "./links";
import type { RouteData } from "./routeData";
import type { SerializeFrom } from "./serialize";

export interface RouteModules<RouteModule> {
[routeId: string]: RouteModule;
Expand Down Expand Up @@ -78,11 +79,67 @@ export interface LoaderFunction {
* A function that returns an object of name + content pairs to use for
* `<meta>` tags for a route. These tags will be merged with (and take
* precedence over) tags from parent routes.
*
* @param Loader - Loader for this meta function's route
* @param ParentsLoaders - Mapping from a parent's route filepath to that route's loader
*
* Note that parent route filepaths are relative to the `app/` directory.
*
* For example, if this meta function is for `/sales/customers/$customerId`:
*
* ```ts
* // app/root.tsx
* const loader = () => {
* return json({ hello: "world" } as const)
* }
* export type Loader = typeof loader
*
* // app/routes/sales.tsx
* const loader = () => {
* return json({ salesCount: 1074 })
* }
* export type Loader = typeof loader
*
* // app/routes/sales/customers.tsx
* const loader = () => {
* return json({ customerCount: 74 })
* }
* export type Loader = typeof loader
*
* // app/routes/sales/customers/$customersId.tsx
* import type { Loader as RootLoader } from "../../../root"
* import type { Loader as SalesLoader } from "../../sales"
* import type { Loader as CustomersLoader } from "../../sales/customers"
*
* const loader = () => {
* return json({ name: "Customer name" })
* }
*
* const meta: MetaFunction<typeof loader, {
* "root": RootLoader,
* "routes/sales": SalesLoader,
* "routes/sales/customers": CustomersLoader,
* }> = ({ data, parentsData }) => {
* const { name } = data
* // ^? string
* const { customerCount } = parentsData["routes/sales/customers"]
* // ^? number
* const { salesCount } = parentsData["routes/sales"]
* // ^? number
* const { hello } = parentsData["root"]
* // ^? "world"
* }
* ```
*/
export interface MetaFunction {
export interface MetaFunction<
Loader extends LoaderFunction | unknown = unknown,
ParentsLoaders extends Record<string, LoaderFunction> = {}
> {
(args: {
data: AppData;
parentsData: RouteData;
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : AppData;
parentsData: {
[k in keyof ParentsLoaders]: SerializeFrom<ParentsLoaders[k]>;
} & RouteData;
params: Params;
location: Location;
}): HtmlMetaDescriptor;
Expand Down

0 comments on commit f793b61

Please sign in to comment.