chrome-devtools-mcpは、AIエージェント(Claudeなど)がChrome DevToolsを操作してWebページのデバッグやパフォーマンス計測を行えるようにするMCPサーバーです。ネットワークログやコンソールメッセージなど大量の結果を返すツールが多いため、結果を「ページ単位」に分割するページネーション処理が随所で使われています。src/utils/pagination.tsはその共通ロジックを1箇所に集めたユーティリティで、外部ライブラリ依存もなく30〜60行で完結しているため、TypeScriptのジェネリクスや早期return、null安全演算子を学ぶのに最適な題材として選びました。
コード
const DEFAULT_PAGE_SIZE = 20;
export function paginate<Item>(
items: readonly Item[],
options?: PaginationOptions,
): PaginationResult<Item> {
const total = items.length;
if (!options || noPaginationOptions(options)) {
return {
items,
currentPage: 0,
totalPages: 1,
hasNextPage: false,
hasPreviousPage: false,
startIndex: 0,
endIndex: total,
invalidPage: false,
};
}
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const {currentPage, invalidPage} = resolvePageIndex(
options.pageIdx,
totalPages,
);
const startIndex = currentPage * pageSize;
const pageItems = items.slice(startIndex, startIndex + pageSize);
const endIndex = startIndex + pageItems.length;
return {
items: pageItems,
currentPage,
totalPages,
hasNextPage: currentPage < totalPages - 1,
hasPreviousPage: currentPage > 0,
startIndex,
endIndex,
invalidPage,
};
}
function noPaginationOptions(options: PaginationOptions): boolean {
return options.pageSize === undefined && options.pageIdx === undefined;
}
function resolvePageIndex(
pageIdx: number | undefined,
totalPages: number,
): {
currentPage: number;
invalidPage: boolean;
} {
if (pageIdx === undefined) {
return {currentPage: 0, invalidPage: false};
}
if (pageIdx < 0 || pageIdx >= totalPages) {
return {currentPage: 0, invalidPage: true};
}
return {currentPage: pageIdx, invalidPage: false};
}
引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。
上から順に読む
export function paginate<Item>(
items: readonly Item[],
options?: PaginationOptions,
): PaginationResult<Item> {`<Item>` はジェネリクス(総称型)という仕組みです。「このItemという名前は呼び出し時に決まる型のプレースホルダー」という意味で、これによりpaginate関数は文字列の配列でもオブジェクトの配列でも同じロジックで扱えます。引数の`items: readonly Item[]`は「Item型の配列だが、この関数内では要素を書き換えない(readonly)」という約束を型で表現したものです。`options?: PaginationOptions`の`?`は「このパラメータは省略可能」という意味で、呼び出し側が渡さなければ`undefined`になります。戻り値の型`PaginationResult<Item>`も同じくItemに応じて中身の型が決まる総称型です。
const total = items.length;
if (!options || noPaginationOptions(options)) {まず`items.length`で配列の要素数を`total`という定数に入れています。次の行の`!options`は「optionsがfalsy(undefinedやnullなど)なら真になる」という否定演算子`!`の使い方です。`||`は論理OR演算子で、左側が真ならその時点で右側`noPaginationOptions(options)`は評価されずに済みます(短絡評価)。つまり「optionsが渡されていない、またはoptionsの中身が空だった」という条件をこの1行で表現しています。
return {
items,
currentPage: 0,
totalPages: 1,
hasNextPage: false,条件に合致したら、ここで関数の実行をすぐ終わらせて値を返します(これを「早期return(ガード節)」と呼びます)。ページ指定がない場合は「全件を1ページ目として返す」という結果を作っています。`items,`のようにキー名だけ書いているのはオブジェクトのショートハンド記法で、`items: items,`と同じ意味です。すべてのフィールドを毎回明示的に埋めているのは、呼び出し側が`PaginationResult`型のどのプロパティにも安心してアクセスできるようにするためです。
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;`??`はnullish coalescing演算子(null合体演算子)です。`||`と似ていますが、左側が`null`または`undefined`のときだけ右側を使う点が違います(`0`や`""`のような値は左側のまま採用される)。ここでは「options.pageSizeが指定されていればそれを使い、指定されていなければファイル冒頭で定義した`DEFAULT_PAGE_SIZE = 20`を使う」というデフォルト値の設定パターンです。
const totalPages = Math.max(1, Math.ceil(total / pageSize));`total / pageSize`は普通の割り算ですが、小数になることがあります(例: 45件を20件ずつなら2.25ページ)。`Math.ceil()`は小数点以下を切り上げる関数なので、2.25は3になります。さらに`Math.max(1, ...)`で「計算結果と1を比べて大きい方を採用する」ことで、要素数が0件でも最低1ページはある、という結果にしています。
const {currentPage, invalidPage} = resolvePageIndex(
options.pageIdx,
totalPages,
);`resolvePageIndex`はこのファイルの下の方で定義されているヘルパー関数で、`{currentPage: number, invalidPage: boolean}`というオブジェクトを返します。左辺の`const {currentPage, invalidPage} = ...`は分割代入(destructuring)と呼ばれる書き方で、返ってきたオブジェクトから同名のプロパティを取り出して、その場で同名の定数として使えるようにしています。
const startIndex = currentPage * pageSize;
const pageItems = items.slice(startIndex, startIndex + pageSize);
const endIndex = startIndex + pageItems.length;`currentPage`は0始まりのページ番号なので、`currentPage * pageSize`で「そのページの最初の要素が元の配列の何番目にあたるか」を計算しています(0ページ目なら0番目から、1ページ目ならpageSize番目から)。`Array.prototype.slice(開始, 終了)`は元の配列を書き換えずに、指定範囲だけを新しい配列としてコピーして返すメソッドです。最後のページは要素数が足りないことがあるため、`pageItems.length`(実際に取れた件数)を使って`endIndex`を計算し直しています。
function noPaginationOptions(options: PaginationOptions): boolean {
return options.pageSize === undefined && options.pageIdx === undefined;
}「ページサイズもページ番号もどちらも指定されていない」という条件を、`&&`(論理AND、両方とも真のときだけ真になる)を使って1つの関数に切り出しています。こうして条件に名前(`noPaginationOptions`)を付けておくと、呼び出し側の`if (!options || noPaginationOptions(options))`が「optionsがない、またはページネーション指定が何もない」と英語のように読めるようになります。これは新卒のうちから真似しやすいテクニックで、複雑な条件式は変数や関数に切り出すと読みやすくなります。
function resolvePageIndex(
pageIdx: number | undefined,
totalPages: number,
): {
currentPage: number;
invalidPage: boolean;
} {戻り値の型を、わざわざ`interface`を定義せずにその場で`{currentPage: number; invalidPage: boolean;}`とオブジェクト形のリテラル型として書いています。これは「この関数専用の、他では使い回さない小さな型」なので、名前を付けるほどでもないという判断です。引数の型`number | undefined`は「numberかundefinedのどちらか」を表すユニオン型(合併型)です。
if (pageIdx === undefined) {
return {currentPage: 0, invalidPage: false};
}ここでも早期returnのガード節が3段構えになっています。1つ目は「ページ番号が指定されていなければ0ページ目で正常」というケースです。この後には「指定されたページ番号が0未満か、総ページ数以上なら範囲外なので0ページ目に戻すが`invalidPage: true`で異常を記録する」ケース、「それ以外(正しい範囲内)ならそのページ番号をそのまま使う」ケースが続きます。呼び出し側はこの`invalidPage`フラグを見て、ユーザーに「指定したページ番号は無効でした」と伝えることができます。