From f793b61935ff00e5a40efc8dab841131881d2f12 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 19 Aug 2022 14:10:34 -0400 Subject: [PATCH] feat: `MetaFunction` type infers `data` and `parentsData` types from 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 --- .changeset/stupid-houses-sing.md | 52 +++++++++++++++ docs/api/conventions.md | 32 +++++++++- packages/remix-server-runtime/routeModules.ts | 63 ++++++++++++++++++- 3 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 .changeset/stupid-houses-sing.md diff --git a/.changeset/stupid-houses-sing.md b/.changeset/stupid-houses-sing.md new file mode 100644 index 00000000000..b6d69be12e1 --- /dev/null +++ b/.changeset/stupid-houses-sing.md @@ -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 = ({ data, parentsData }) => { + const { name } = data + // ^? string + const { customerCount } = parentsData["routes/sales/customers"] + // ^? number + const { salesCount } = parentsData["routes/sales"] + // ^? number + const { hello } = parentsData["root"] + // ^? "world" +} diff --git a/docs/api/conventions.md b/docs/api/conventions.md index 8a1a48b24a6..34560ed881b 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -1072,7 +1072,7 @@ 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 = ({ data, params }) => { if (!data) { return { title: "Missing Shake", @@ -1080,7 +1080,7 @@ export const meta: MetaFunction = ({ data, params }) => { }; } - const { shake } = data as LoaderData; + const { shake } = data; return { title: `${shake.name} milkshake`, description: shake.summary, @@ -1088,6 +1088,34 @@ export const meta: MetaFunction = ({ data, params }) => { }; ``` +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 = ({ data, parentsData }) => { + const { name } = data + // ^? string + const { salesCount } = parentsData["routes/sales"] + // ^? number +} +``` + ### `links` The links function defines which `` elements to add to the page when the user visits a route. diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index b6bdf23c74a..ec228386866 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -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 { [routeId: string]: RouteModule; @@ -78,11 +79,67 @@ export interface LoaderFunction { * A function that returns an object of name + content pairs to use for * `` 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 = ({ 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 = {} +> { (args: { - data: AppData; - parentsData: RouteData; + data: Loader extends LoaderFunction ? SerializeFrom : AppData; + parentsData: { + [k in keyof ParentsLoaders]: SerializeFrom; + } & RouteData; params: Params; location: Location; }): HtmlMetaDescriptor;