Created
March 3, 2026 15:24
-
-
Save alaindet/7fcc60e2405e38fbef1b0c5db38e1d2b to your computer and use it in GitHub Desktop.
Derive pagination in pure TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { defaultDerivedPagination, derivePagination, PaginationDerivedData } from './derive-pagination'; | |
| describe('derivePagination()', () => { | |
| it('rejects invalid page input', () => { | |
| expect(() => derivePagination({ page: -1, pageSize: 1, totalItems: 1 })).toThrowError(); | |
| }); | |
| it('rejects invalid pageSize input', () => { | |
| expect(() => derivePagination({ page: 1, pageSize: -1, totalItems: 1 })).toThrowError(); | |
| }); | |
| it('rejects invalid totalItems input', () => { | |
| expect(() => derivePagination({ page: 1, pageSize: 1, totalItems: -1 })).toThrowError(); | |
| }); | |
| it('rejects page input is greater than the last allowed page', () => { | |
| expect(() => derivePagination({ page: 420, pageSize: 10, totalItems: 42 })).toThrowError(); | |
| }); | |
| it('defaults to one page if page size is greater than total', () => { | |
| const res = derivePagination({ page: 1, pageSize: 100, totalItems: 42 }); | |
| const expected = { | |
| hasNextPage: false, | |
| hasPreviousPage: false, | |
| isLastPage: true, | |
| lastPage: 1, | |
| pageInf: 1, | |
| pageSup: 42, | |
| }; | |
| expect(compareDerivedPaginations(res, expected)).toBeTrue(); | |
| }); | |
| it('calculates the total pages correctly 1/2', () => { | |
| const res = derivePagination({ page: 1, pageSize: 10, totalItems: 30 }); | |
| const expected = { | |
| hasNextPage: true, | |
| hasPreviousPage: false, | |
| isLastPage: false, | |
| lastPage: 3, | |
| pageInf: 1, | |
| pageSup: 10, | |
| }; | |
| expect(compareDerivedPaginations(res, expected)).toBeTrue(); | |
| }); | |
| it('calculates the total pages correctly 2/2', () => { | |
| const res = derivePagination({ page: 1, pageSize: 10, totalItems: 31 }); | |
| const expected = { | |
| hasNextPage: true, | |
| hasPreviousPage: false, | |
| isLastPage: false, | |
| lastPage: 4, | |
| pageInf: 1, | |
| pageSup: 10, | |
| }; | |
| expect(compareDerivedPaginations(res, expected)).toBeTrue(); | |
| }); | |
| it('calculates the page limits of a mid page', () => { | |
| const res = derivePagination({ page: 3, pageSize: 4, totalItems: 14 }); | |
| const expected = { | |
| hasNextPage: true, | |
| hasPreviousPage: true, | |
| isLastPage: false, | |
| lastPage: 4, | |
| pageInf: 9, | |
| pageSup: 12, | |
| }; | |
| expect(compareDerivedPaginations(res, expected)).toBeTrue(); | |
| }); | |
| it('calculates the page limits of the last page', () => { | |
| const res = derivePagination({ page: 4, pageSize: 4, totalItems: 14 }); | |
| const expected = { | |
| hasNextPage: false, | |
| hasPreviousPage: true, | |
| isLastPage: true, | |
| lastPage: 4, | |
| pageInf: 13, | |
| pageSup: 14, | |
| }; | |
| expect(compareDerivedPaginations(res, expected)).toBeTrue(); | |
| }); | |
| }); | |
| fdescribe('derivePagination.orDefault()', () => { | |
| it('returns default for invalid page input', () => { | |
| const res = derivePagination.orDefault({ page: -1, pageSize: 1, totalItems: 1 }); | |
| expect(isDefaultDerivedPagination(res)).toBeTrue(); | |
| }); | |
| it('returns default for invalid pageSize input', () => { | |
| const res = derivePagination.orDefault({ page: 1, pageSize: -1, totalItems: 1 }); | |
| expect(isDefaultDerivedPagination(res)).toBeTrue(); | |
| }); | |
| it('returns default for invalid totalItems input', () => { | |
| const res = derivePagination.orDefault({ page: 1, pageSize: -1, totalItems: -1 }); | |
| expect(isDefaultDerivedPagination(res)).toBeTrue(); | |
| }); | |
| it('returns default when page input is greater than the last allowed page', () => { | |
| const res = derivePagination.orDefault({ page: 420, pageSize: 10, totalItems: 42 }); | |
| expect(isDefaultDerivedPagination(res)).toBeTrue(); | |
| }); | |
| }); | |
| // Local test helper | |
| function compareDerivedPaginations( | |
| left: PaginationDerivedData, | |
| right: PaginationDerivedData, | |
| ): boolean { | |
| return normalizeDerivedPagination(left) === normalizeDerivedPagination(right); | |
| } | |
| // Local test helper | |
| function normalizeDerivedPagination(data: PaginationDerivedData): string { | |
| // This ensures object has the same key order | |
| return JSON.stringify({ | |
| hasNextPage: data.hasNextPage, | |
| hasPreviousPage: data.hasPreviousPage, | |
| isLastPage: data.isLastPage, | |
| lastPage: data.lastPage, | |
| pageInf: data.pageInf, | |
| pageSup: data.pageSup, | |
| }); | |
| } | |
| // Local test helper | |
| const defaultNormalized = normalizeDerivedPagination(defaultDerivedPagination); | |
| function isDefaultDerivedPagination(data: PaginationDerivedData): boolean { | |
| return normalizeDerivedPagination(data) === defaultNormalized; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export type PaginationParams = { | |
| page: number; | |
| pageSize: number; | |
| totalItems: number; | |
| }; | |
| export type PaginationDerivedData = { | |
| hasPreviousPage: boolean; | |
| hasNextPage: boolean; | |
| lastPage: number; | |
| isLastPage: boolean; | |
| pageInf: number; | |
| pageSup: number; | |
| }; | |
| export const defaultDerivedPagination: PaginationDerivedData = { | |
| hasPreviousPage: false, | |
| hasNextPage: false, | |
| lastPage: 1, | |
| isLastPage: true, | |
| pageInf: 0, | |
| pageSup: 0, | |
| }; | |
| export function derivePagination( | |
| { page, pageSize, totalItems }: PaginationParams, | |
| ): PaginationDerivedData { | |
| if (page <= 0) { | |
| throw new Error('derivePagination(): invalid page input'); | |
| } | |
| if (pageSize <= 0) { | |
| throw new Error('derivePagination(): invalid pageSize input'); | |
| } | |
| if (totalItems <= 0) { | |
| throw new Error('derivePagination(): invalid totalItems input'); | |
| } | |
| const pagesCount = Math.floor(totalItems / pageSize); | |
| const remaining = totalItems > (pagesCount * pageSize) ? 1 : 0; | |
| const lastPage = pagesCount + remaining; | |
| if (page > lastPage) { | |
| throw new Error('derivePagination(): page exceeds the last allowed page'); | |
| } | |
| const hasPreviousPage = page > 1; | |
| const hasNextPage = page < lastPage; | |
| const pageInf = Math.min(pageSize * (page - 1) + 1); | |
| const pageSup = Math.min(pageSize * page, totalItems); | |
| return { | |
| hasPreviousPage, | |
| hasNextPage, | |
| lastPage, | |
| isLastPage: page === lastPage, | |
| pageInf, | |
| pageSup, | |
| }; | |
| } | |
| // Same logic, but it returns a default value instead of throwing an error | |
| derivePagination.orDefault = (params: PaginationParams): PaginationDerivedData => { | |
| try { | |
| return derivePagination(params); | |
| } catch (err) { | |
| return defaultDerivedPagination; | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment