feat(paginate): Return exact types from paginate() (#8229)

* feat(paginate): Return exact types from paginate()

* chore: changeset

* fix(types): Fix infer utils while I'm here

---------

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
Erika 2023-08-28 18:02:05 +02:00 committed by GitHub
parent 57e9a28063
commit ffc9e2d3de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 53 additions and 13 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Paginate will now return exact types instead of a naive Record

View file

@ -57,7 +57,7 @@ declare module 'astro:assets' {
}; };
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }; type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] }; type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
type ImgAttributes = WithRequired< type ImgAttributes = WithRequired<
Omit<import('./types').HTMLAttributes<'img'>, 'src' | 'width' | 'height'>, Omit<import('./types').HTMLAttributes<'img'>, 'src' | 'width' | 'height'>,
'alt' 'alt'

View file

@ -1600,7 +1600,9 @@ export type GetStaticPaths = (
* const { slug } = Astro.params as Params; * const { slug } = Astro.params as Params;
* ``` * ```
*/ */
export type InferGetStaticParamsType<T> = T extends () => infer R | Promise<infer R> export type InferGetStaticParamsType<T> = T extends (
opts?: GetStaticPathsOptions
) => infer R | Promise<infer R>
? R extends Array<infer U> ? R extends Array<infer U>
? U extends { params: infer P } ? U extends { params: infer P }
? P ? P
@ -1631,7 +1633,9 @@ export type InferGetStaticParamsType<T> = T extends () => infer R | Promise<infe
* const { propA, propB } = Astro.props; * const { propA, propB } = Astro.props;
* ``` * ```
*/ */
export type InferGetStaticPropsType<T> = T extends () => infer R | Promise<infer R> export type InferGetStaticPropsType<T> = T extends (
opts: GetStaticPathsOptions
) => infer R | Promise<infer R>
? R extends Array<infer U> ? R extends Array<infer U>
? U extends { props: infer P } ? U extends { props: infer P }
? P ? P
@ -1678,13 +1682,13 @@ export type MarkdownContent<T extends Record<string, any> = Record<string, any>>
* *
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#paginate) * [Astro reference](https://docs.astro.build/en/reference/api-reference/#paginate)
*/ */
export interface PaginateOptions { export interface PaginateOptions<PaginateProps extends Props, PaginateParams extends Params> {
/** the number of items per-page (default: `10`) */ /** the number of items per-page (default: `10`) */
pageSize?: number; pageSize?: number;
/** key: value object of page params (ex: `{ tag: 'javascript' }`) */ /** key: value object of page params (ex: `{ tag: 'javascript' }`) */
params?: Params; params?: PaginateParams;
/** object of props to forward to `page` result */ /** object of props to forward to `page` result */
props?: Props; props?: PaginateProps;
} }
/** /**
@ -1718,7 +1722,33 @@ export interface Page<T = any> {
}; };
} }
export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStaticPathsResult; type OmitIndexSignature<ObjectType> = {
// eslint-disable-next-line @typescript-eslint/ban-types
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown>
? never
: KeyType]: ObjectType[KeyType];
};
// eslint-disable-next-line @typescript-eslint/ban-types
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
export type PaginateFunction = <
PaginateData,
AdditionalPaginateProps extends Props,
AdditionalPaginateParams extends Params,
>(
data: PaginateData[],
args?: PaginateOptions<AdditionalPaginateProps, AdditionalPaginateParams>
) => {
params: Simplify<
{
page: string | undefined;
} & OmitIndexSignature<AdditionalPaginateParams>
>;
props: Simplify<
{
page: Page<PaginateData>;
} & OmitIndexSignature<AdditionalPaginateProps>
>;
}[];
export type Params = Record<string, string | undefined>; export type Params = Record<string, string | undefined>;

View file

@ -1,18 +1,20 @@
import type { import type {
GetStaticPathsResult,
Page, Page,
PaginateFunction, PaginateFunction,
PaginateOptions,
Params, Params,
Props, Props,
RouteData, RouteData,
} from '../../@types/astro'; } from '../../@types/astro';
import { AstroError, AstroErrorData } from '../errors/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js';
export function generatePaginateFunction(routeMatch: RouteData): PaginateFunction { export function generatePaginateFunction(
routeMatch: RouteData
): (...args: Parameters<PaginateFunction>) => ReturnType<PaginateFunction> {
return function paginateUtility( return function paginateUtility(
data: any[], data: any[],
args: { pageSize?: number; params?: Params; props?: Props } = {} args: PaginateOptions<Props, Params> = {}
) { ): ReturnType<PaginateFunction> {
let { pageSize: _pageSize, params: _params, props: _props } = args; let { pageSize: _pageSize, params: _params, props: _props } = args;
const pageSize = _pageSize || 10; const pageSize = _pageSize || 10;
const paramName = 'page'; const paramName = 'page';
@ -31,7 +33,7 @@ export function generatePaginateFunction(routeMatch: RouteData): PaginateFunctio
} }
const lastPage = Math.max(1, Math.ceil(data.length / pageSize)); const lastPage = Math.max(1, Math.ceil(data.length / pageSize));
const result: GetStaticPathsResult = [...Array(lastPage).keys()].map((num) => { const result = [...Array(lastPage).keys()].map((num) => {
const pageNum = num + 1; const pageNum = num + 1;
const start = pageSize === Infinity ? 0 : (pageNum - 1) * pageSize; // currentPage is 1-indexed const start = pageSize === Infinity ? 0 : (pageNum - 1) * pageSize; // currentPage is 1-indexed
const end = Math.min(start + pageSize, data.length); const end = Math.min(start + pageSize, data.length);

View file

@ -3,6 +3,7 @@ import type {
GetStaticPathsItem, GetStaticPathsItem,
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPathsResultKeyed, GetStaticPathsResultKeyed,
PaginateFunction,
Params, Params,
RouteData, RouteData,
RuntimeMode, RuntimeMode,
@ -50,7 +51,9 @@ export async function callGetStaticPaths({
// Calculate your static paths. // Calculate your static paths.
let staticPaths: GetStaticPathsResult = []; let staticPaths: GetStaticPathsResult = [];
staticPaths = await mod.getStaticPaths({ staticPaths = await mod.getStaticPaths({
paginate: generatePaginateFunction(route), // Q: Why the cast?
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
paginate: generatePaginateFunction(route) as PaginateFunction,
rss() { rss() {
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper); throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
}, },