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 Simplify<T> = { [KeyType in keyof T]: T[KeyType] };
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
type ImgAttributes = WithRequired<
Omit<import('./types').HTMLAttributes<'img'>, 'src' | 'width' | 'height'>,
'alt'

View file

@ -1600,7 +1600,9 @@ export type GetStaticPaths = (
* 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>
? U extends { params: infer P }
? P
@ -1631,7 +1633,9 @@ export type InferGetStaticParamsType<T> = T extends () => infer R | Promise<infe
* 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>
? U extends { props: infer 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)
*/
export interface PaginateOptions {
export interface PaginateOptions<PaginateProps extends Props, PaginateParams extends Params> {
/** the number of items per-page (default: `10`) */
pageSize?: number;
/** key: value object of page params (ex: `{ tag: 'javascript' }`) */
params?: Params;
params?: PaginateParams;
/** 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>;

View file

@ -1,18 +1,20 @@
import type {
GetStaticPathsResult,
Page,
PaginateFunction,
PaginateOptions,
Params,
Props,
RouteData,
} from '../../@types/astro';
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(
data: any[],
args: { pageSize?: number; params?: Params; props?: Props } = {}
) {
args: PaginateOptions<Props, Params> = {}
): ReturnType<PaginateFunction> {
let { pageSize: _pageSize, params: _params, props: _props } = args;
const pageSize = _pageSize || 10;
const paramName = 'page';
@ -31,7 +33,7 @@ export function generatePaginateFunction(routeMatch: RouteData): PaginateFunctio
}
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 start = pageSize === Infinity ? 0 : (pageNum - 1) * pageSize; // currentPage is 1-indexed
const end = Math.min(start + pageSize, data.length);

View file

@ -3,6 +3,7 @@ import type {
GetStaticPathsItem,
GetStaticPathsResult,
GetStaticPathsResultKeyed,
PaginateFunction,
Params,
RouteData,
RuntimeMode,
@ -50,7 +51,9 @@ export async function callGetStaticPaths({
// Calculate your static paths.
let staticPaths: GetStaticPathsResult = [];
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() {
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
},