Skip to content

Instantly share code, notes, and snippets.

@alaindet
Created March 3, 2026 15:24
Show Gist options
  • Select an option

  • Save alaindet/7fcc60e2405e38fbef1b0c5db38e1d2b to your computer and use it in GitHub Desktop.

Select an option

Save alaindet/7fcc60e2405e38fbef1b0c5db38e1d2b to your computer and use it in GitHub Desktop.
Derive pagination in pure TypeScript
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;
}
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