kintone のカスタマイズ開発を「ビルドは挟まない、でもエディタ上では型補完と型チェックが効く」状態で進めるためのテンプレートリポジトリ Mr. Satan を公開しました。GitHub の「Use this template」からそのまま派生リポジトリを作って使えます。
本記事ではこのテンプレートの位置づけと、核となる「.js から types.ts を JSDoc で参照する」パターンを具体例で解説します。
kintone カスタマイズの現実:2 つの極端
kintone のカスタマイズ開発には、大きく分けて 2 つの方向性があります。
1. webpack などでバンドルしてアップロード
TypeScript・最新 JS 構文・npm エコシステムをフル活用できる反面、kintone にアップロードされるのはバンドル後の成果物。そのコードを直接編集することは事実上不可能で、開発環境を持っている担当者でないと手を入れられません。拙作 Goqoo on kintone はこの方向性の代表例です。
本格開発には最適ですが、「定数を一文字書き換えたい」だけでも再ビルド・再アップロードが必要になります。
2. 素の .js をそのままアップロード
kintone 上で直接編集できるので手軽ですが、型補完・型チェック・モジュール分割といったモダンな開発体験は諦めることになります。フィールド名をタイポしてもエディタは何も教えてくれません。
どちらも一長一短で、「納品後に顧客側で定数を差し替えてほしい」「保守契約なしで納品完結する案件」のようなシチュエーションでは、前者は過剰で、後者は辛い、という状態が続いていました。
3 つ目の選択肢:型は書く、ただしビルドはしない
Mr. Satan はその中間を狙ったテンプレートです。
-
.jsをビルドせずそのまま kintone にアップロード → kintone 上で直接編集できる余地を残す - 開発中は
types.tsに型定義を書き、.jsからは JSDoc の@type {import('./types').Foo}で参照 → VS Code 上では型補完・型チェックが効く - ビルド工程がないので「ソースと本番の同期ズレ」が原理的に起きない
- npmでのscaffoldはあえて採用せず、戦闘力低めのGitHubテンプレートに留めた
以降、この仕組みのコアとなる types.ts + JSDoc 参照を具体例で見ていきます。
具体例:.js から types.ts を JSDoc で参照する
テンプレートに同梱されているサンプルアプリ(src/apps/sample1/)を題材に、フィールド型定義から JSDoc 参照までの流れを追います。
Step 1: trunks でフィールド型を生成する
kintone のアプリからフィールド型定義を自動生成するのに、拙作 @goqoo/trunks を使います。@kintone/dts-gen のラッパーで、trunks.config.ts に複数アプリを登録しておくと一発で全アプリの型を出力できます。
trunks 自体について詳しくは別記事 @kintone/dts-gen を複数アプリで一括実行するラッパーツール @goqoo/trunks を作った を参照してください。
// trunks.config.ts
import { defineConfig } from '@goqoo/trunks';
export default defineConfig({
host: 'your-subdomain.cybozu.com',
apps: {
sample1: 1,
},
auth: { type: 'password' },
});
npx @goqoo/trunks を実行すると dts/sample1-fields.d.ts が生成されます。
// dts/sample1-fields.d.ts(抜粋)
declare namespace kintone.types {
interface Sample1Fields {
件名: kintone.fieldTypes.SingleLineText;
本文: kintone.fieldTypes.MultiLineText;
担当者: kintone.fieldTypes.UserSelect;
期日: kintone.fieldTypes.Date;
}
interface SavedSample1Fields extends Sample1Fields {
$id: kintone.fieldTypes.Id;
$revision: kintone.fieldTypes.Revision;
// ... システムフィールド一式
}
}
ここで生成されるのは kintone 側のフィールド定義そのもの。日本語のフィールド名もそのまま型になります。
Step 2: types.ts でイベント型を組み立てる
kintone のイベント型は 2 層に分けて管理します。汎用的な部分(kintone 側の共通構造)は 全アプリ共通の src/types.ts に一度だけ書き、各アプリの src/apps/{app}/types.ts はそこから import type で引き込んでアプリ固有のレコード型を食わせるだけ、という二段構え。
まず汎用側:
// src/types.ts
export type KintoneEvent = {
appId: number;
type: string;
error?: string;
};
export type IndexEvent<T> = KintoneEvent & {
records: T[];
viewType: 'list' | 'calendar' | 'custom';
viewId: number;
viewName: string;
offset: number | null;
size: number | null;
date: string | null;
};
export type DetailEvent<T> = KintoneEvent & {
record: T;
recordId: number;
reuse?: boolean;
};
export type SubmitEvent<T> = KintoneEvent & {
record: T;
changes?: {
field: { type: string; value: unknown };
row?: { id: string; value: Record<string, unknown> };
};
};
次にアプリ固有側:
// src/apps/sample1/types.ts
import type { IndexEvent, DetailEvent, SubmitEvent } from '../../types';
// Step 1 で生成された型を流用
export type Sample1Record = kintone.types.SavedSample1Fields;
// イベント型はレコード型を食わせるだけ
export type Sample1IndexEvent = IndexEvent<Sample1Record>;
export type Sample1DetailEvent = DetailEvent<Sample1Record>;
export type Sample1SubmitEvent = SubmitEvent<Sample1Record>;
アプリの types.ts が薄く保てるのがポイント。アプリが 10 個あっても、kintone のイベント構造の定義は src/types.ts に一度書けば使い回せます。新規アプリを追加するときも、アプリ側に書くのは「レコード型を食わせたイベント型のエイリアス」だけで済みます。
Step 3: .js から JSDoc で types.ts を参照する
ここが本題です。実装用の .js 側では、ファイル先頭に //@ts-check を付けた上で、イベントハンドラの引数に JSDoc で型を与えます。
以下の例は edit-show.js の抜粋。前提として sample1 には common.js を 1 本置いてあり、同じアプリ内の複数ファイルから使い回したいユーティリティ(ここでは hasValue)を window.__sample1Common 経由で公開しています。edit-show.js はその __sample1Common から hasValue を取り出して使います。
//@ts-check
// src/apps/sample1/edit-show.js
{
const { hasValue } = window.__sample1Common;
kintone.events.on(
['app.record.create.show', 'app.record.edit.show'],
(/** @type {import('./types').Sample1DetailEvent} */ event) => {
const { record } = event;
// 件名(SingleLineText)の value にアクセス — IDE で補完が効く
if (hasValue(record.件名)) {
record.本文.disabled = true;
}
// 期日(Date)が空なら今日の日付を入れる
if (!hasValue(record.期日)) {
record.期日.value = luxon.DateTime.now().toFormat('yyyy-MM-dd');
}
return event;
},
);
}
ポイントは (/** @type {import('./types').Sample1DetailEvent} */ event) => の部分。JSDoc の @type で types.ts から Sample1DetailEvent を import することで、それ以降 event.record.件名.value のような形で型補完とタイポチェックがフルに効きます。
実ファイルはあくまで素の .js なので、そのまま kintone にアップロードできます。ビルド成果物ではないので、アップロード後に kintone 側で直接編集して定数をいじる、といった運用も可能です。
sample1/ アプリは common.js / edit-show.js / index-show.js の 3 本構成で、これらを同じ kintone アプリの「JavaScript / CSS でカスタマイズ」に登録して使います。common.js は後続ファイルから参照している都合上、先頭に並べてください。この点は kintone カスタマイズ界隈で普通に行われている方法ですね。
もう一つの sample2/ アプリは edit-submit.js だけで完結する最小構成になっています。共通ヘルパを切り出す必要がない規模のアプリでは common.js は省略して構いません。
さらに、複数アプリ横断で使いたいユーティリティのために src/global.js という全アプリ共通 JS も同梱しています。kintoneシステム管理の「JavaScript / CSSでカスタマイズ」で登録すると、どのアプリの JS からも window.__global で参照できます。ヘルパの置き場所は以下の 3 階層で使い分けるイメージです。
| スコープ | 置き場所 | 参照 | 例 |
|---|---|---|---|
| 全アプリ横断 | src/global.js |
window.__global |
ログ関数 log(tag, ...)
|
| アプリ内共通 | src/apps/{app}/common.js |
window.__{app}Common |
sample1 の hasValue / today
|
| ファイル内ローカル | IIFE ブロック内 | 直接参照 | sample2 の isValidEmail
|
比較:全部インラインの JSDoc で書くと?
types.ts + import 方式には、「インラインで JSDoc にオブジェクト型リテラルを直接書く」という代替があります。ただしその方式は現実的に辛いです。
// 悪い例:インラインで全部書こうとした場合
kintone.events.on(
['app.record.edit.show'],
/** @param {{ appId: number, record: { 件名: { type: 'SINGLE_LINE_TEXT', value: string }, 本文: { type: 'MULTI_LINE_TEXT', value: string, disabled?: boolean }, 期日: { type: 'DATE', value: string } /* ... */ }, recordId: number, type: string }} event */
(event) => { ... }
);
この書き方の問題点:
- 一行が暴発する。フィールド数が増えると JSDoc コメントだけで画面が埋まる
- 他のイベント(
submit、index.showなど)でも同じ型を書き直す羽目になる - フィールド定義が変わったらすべての JSDoc を手で直さなければならない
一方、types.ts に一度だけ定義を書く方式なら、JSDoc 側は import('./types').Sample1DetailEvent という 1 行の参照だけで済みます。types.ts を一箇所直せば、それを import している全 .js に自動的に波及します。
TS で書ける部分は TS で、JSDoc は参照だけ。これがこのスタイルの肝です。
なぜ今このスタイルが現実的になったのか
実は .js + JSDoc + types.ts という構成自体は何年も前に思いついていて、Goqoo on kintoneの設定ファイル goqoo.config.js / goqoo.config.types.ts でもこのパターンを採用しています。
しかし、ロジックを書くJSにこのパターンを適用しようとすると今まではうまく行きませんでした。シンプルな設定ファイルに比べると、変数・関数に細かく型を当てていく作業が沢山あるし、型の作りが特殊になるケースもそれなりに出てきます。
@type {import('./types').Foo} の付け直しや、types.ts 側の変更への追従といったJSDoc のメンテナンスは、人手でやると地味に頭を使う作業です。「この JSDoc は正しく解決されているか?」「import パスは合っているか?」と確認していく途中で気持ちが折れて、結局は「全部 TypeScript にして webpack でビルドするわ!インラインで型が書けるのサイコー!」と結論づくことばかりでした。
Claude Code をはじめとする生成 AI は、この種の機械的な追従作業を苦にしません。むしろ得意分野です。JSDoc メンテのコスト問題が AI 側に吸収されるので、人間は「どのような型を types.ts に置くか」という本質的な設計だけに集中できます。
このテンプレートを公開するに至った動機の一つが、まさにここにあります。以前何度か諦めたこのパターンが、AIのお陰でようやく実案件でも採用できるようになりました。そんな経緯があるので、Mr. Satanは生成 AI と協働する前提で使うことを推奨します(もちろん人手だけでも動作します)。
リポジトリには AI 向けのガイドとして CLAUDE.md を同梱してあり、Claude Code から参照される前提で規約・命名・落とし穴(common.js の読み込み順、Window インターフェース拡張パターン、kintone API を推測で書かない、など)を整理しています。
使い方
- GitHub の「Use this template」ボタンから自分のリポジトリを作る
npm install-
.ginuerc.jsのドメインとappIdを実プロジェクトに差し替え -
npx @goqoo/trunks initでtrunks.config.tsを対話的に作成 -
.envまたは~/.netrc(.gpg)に kintone の認証情報を記入 -
npx @goqoo/trunksでフィールド型定義を生成、npx ginue pull developmentで設定を取り込み -
src/apps/sample1/(共通ヘルパ + 複数機能ファイル)かsrc/apps/sample2/(1 ファイル完結)の好きな方をコピーして自プロジェクトのアプリ用ディレクトリを作る
詳細な手順やディレクトリ構成、新規アプリ追加の流れは README.md / CLAUDE.md を参照してください。
アプリ設計情報も Git で管理する:ginue
使い方の step 6 に npx ginue pull が登場しましたが、これも拙作のツール ginue です。
kintone のカスタマイズ開発では、JavaScript のコードだけでなく、アプリのフィールド定義・フォームレイアウト・プロセス管理・アクセス権などの設定を触る場面も多々あります。これらをローカルに JSON として保存して Git で追跡し、開発環境と本番環境の差分管理・デプロイを行うのに ginue を使います。
# 開発環境からアプリ設定を pull
npx ginue pull development
# 特定アプリのみ
npx ginue pull development -A sample1
# 開発環境から本番環境へ push
npx ginue push development:production -A sample1
テンプレートでは kintone-settings/development/ / kintone-settings/production/ 配下にファイルが吐き出され、「アプリ設定の変更を PR にしてレビュー → 開発環境で検証 → 本番環境へ push」という流れをコードベースから一元的に扱えるようになります。.ginuerc.js に環境ごとのドメインとアプリ ID を書いておくだけで使い始められます。
@goqoo/trunks が「コード側の型」を kintone から引っ張ってくる役割だとすれば、ginue は「アプリ設定そのもの」を kintone と同期する役割。両者を組み合わせることで、コードもアプリ設計も丸ごと Git で扱える開発体制になります。
手前味噌ですが、個人的にはginue なしでは kintone 開発がまともに回せないくらい、どの案件でも必ず手元に置いているツールです。
まとめ
- kintone カスタマイズは「webpack でフルビルド」と「素の JS」の二択になりがち
- 中間案として「
.jsをそのまま上げる、ただしtypes.ts+ JSDoc で型は効かせる」がある - このスタイルは JSDoc メンテコストで人間単独だと続きにくかったが、生成 AI 時代になって現実的になった
- コード側は
@goqoo/trunksで型生成、アプリ設計側はginueで管理、とツールを組み合わせて丸ごと追跡できる - Mr. Satan はそのためのテンプレート。Claude Code 等と併用する前提で使えます
本格開発なら Goqoo、軽量で編集余地を残したいなら Satan、と使い分けてもらえれば幸いです。
最後にひとつ、叫ばせてください
ちょっと別の話なんですけどねぇ、kintoneカスタマイズのベストプラクティスに沿って、このテンプレートの .js もすべて { ... } の IIFE ブロックで囲んでいたり、共通ユーティリティを window.__xxxCommon というブラ下げ方式で公開していたりするわけですけど、、、kintone のカスタマイズ JS を type="module" として読み込む方法がないから未だにこんな書き方しなきゃいけないんですよ。
素直に ES モジュールが使えるなら、
// こう書きたい
import { hasValue, today } from './common.js';
で済むところを、今は window オブジェクトの片隅に値をぶら下げる昔ながらの作法でしのいでいます。
ちなみにこの window.__xxx 周りの管理(common.js への追加と Window インターフェース拡張の同期、ファイル読み込み順の維持など)も、前述の JSDoc メンテと同じく 人手だと地味に辛いけれど生成 AI は平気でこなしてくれる 系の作業です。Mr. Satan がまともな開発体験として成立するのは、型安全に限らずこうした雑務まで AI が裏で引き受けてくれているから、という側面がかなり大きい。とはいえ、そもそも要らないなら無いに越したことはないので、
サイボウズさん、早く kintone カスタマイズで type="module" の JavaScript が適用できるようにしてください! そうなれば IIFE と window.__xxx 一式は一掃できて、もっと普通の JavaScript として書けるようになります。切実にお願いします。