業務で Notion のデータベースを活用している場合、Google スプレッドシートへ自動出力・同期できると、生産性が格段に向上します。Notion の API を使えば、データベースの読み書きを自由に操作できますが、GAS(Google Apps Script)を使って Notion とシートを連携させることで、誰でも簡単かつ自動的に整ったデータ管理が可能です。本記事では、「Notion データベース ↔ Google スプレッドシート」を GAS で連携する方法を、clasp/TypeScript のローカル開発環境と Script Properties を用いた秘匿設定を含め、導入から実装、運用上の注意点まで具体的に解説します。
[目次を開く]
1. 開発環境の準備:clasp と TypeScript を使ったローカルセットアップ
1‑1. clasp の導入と初期設定
まずローカルで開発管理するために、clasp をインストールします:
npm install -g @google/clasp@latest
clasp login clasp loginでブラウザがたちあがるので、そちらで任意のGoogleアカウントと連携してください。
続いて Notion⇄スプレッド連携用の GAS プロジェクトを作成します:
mkdir notion‑sheet‑sync
cd notion‑sheet‑sync
clasp create --type sheets --title "Notion‑Sheet‑Sync" これにより .clasp.json と appsscript.json が生成され、clasp による管理が可能になります。
以下のようになっていれば成功です。
1‑2. TypeScript を導入して型安全なコードを書く
以下の内容でローカル開発環境を整えます:
npm install --save-dev typescript @types/google-apps-script
npx tsc --init tsconfig.json を次のように設定し、src/ ディレクトリに .ts ファイルを配置します:
{
"compilerOptions": {
"target": "ES2020",
"module": "none",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"types": ["google-apps-script"]
},
"include": ["src/**/*"]
} TypeScript で書いたコードは npx tsc によって dist/main.js にトランスパイルされ、clasp push で GAS に展開されます。
以下は npx tsc 後のディレクトリ構成です。
2. Notion API 連携の前提:Integration、データベース作成、スクリプトプロパティ
2‑1. Notion で Integration を作成してトークン取得
Notion の My integrations 画面から Integration を新規作成し、「Internal Integration Token」を取得します。これは外部GASから API を呼び出す際のキーとなります。
以下のURLからアクセスできます
https://www.notion.so/profile/integrations
内部インテグレーションシークレットはセキュリティリスクになるので外部に漏洩しないように気を付けましょう。またコンテンツ機能についても基本的には最小限のものを許可するようにしてください。
2‑2. データベースの作成・Integration を招待
Notion 上で操作対象となるテーブル(データベース)を作成し、右上の「・・・」→「接続」で先ほど作成した Integration を呼び出してアクセスを許可します。
2‑3. データベースID の取得と Script Properties への登録
clasp open-scriptを実行しスクリプトを開きましょう。その後、左下の歯車マークをクリックしプロジェクトの設定下部にあるスクリプト プロパティにNOTION_DB_IDとNOTION_TOKENを登録してください。こうすることで秘匿情報をより安全に管理することができます。
3. GAS による Notion ⇄ スプレッドシート同期機能の実装(TypeScript)
src/main.ts に以下のような構成で関数を実装します:
3‑1. fetchNotionToSheet(Notion→スプレッドシートへの取得)
async function fetchNotionToSheet(): Promise<void> {
const props = PropertiesService.getScriptProperties();
const token = props.getProperty('NOTION_TOKEN')!;
const dbId = props.getProperty('NOTION_DB_ID')!;
const notionVersion = '2022-06-28';
// データベースのプロパティ取得
const dbMeta = UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${dbId}`, {
method: 'get',
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': notionVersion
}
});
const dbData = JSON.parse(dbMeta.getContentText());
const propertyOrder: string[] = Object.keys(dbData.properties);
// すべてのページデータを格納する配列
const allResults: any[] = [];
let hasMore = true;
let nextCursor: string | null = null;
while (hasMore) {
const payload: any = {};
if (nextCursor) {
payload.start_cursor = nextCursor;
}
const queryRes = UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${dbId}/query`, {
method: 'post',
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': notionVersion,
'Content-Type': 'application/json'
},
payload: JSON.stringify(payload)
});
const data = JSON.parse(queryRes.getContentText());
allResults.push(...data.results);
hasMore = data.has_more;
nextCursor = data.next_cursor;
}
if (!allResults.length) return;
const rows = allResults.map(page =>
propertyOrder.map(name => {
const prop = page.properties[name];
switch (prop?.type) {
case 'title': return prop.title?.[0]?.plain_text || '';
case 'rich_text': return prop.rich_text?.[0]?.plain_text || '';
case 'date': return prop.date?.start || '';
case 'number': return prop.number?.toString() || '';
case 'select': return prop.select?.name || '';
case 'multi_select': return prop.multi_select?.map((s: any) => s.name).join(', ');
case 'checkbox': return prop.checkbox ? 'TRUE' : 'FALSE';
case 'url': return prop.url || '';
case 'status': return prop.status?.name || '';
default: return '';
}
})
);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('NotionExport') ||
SpreadsheetApp.getActiveSpreadsheet().insertSheet('NotionExport');
sheet.clearContents();
sheet.appendRow(propertyOrder);
sheet.getRange(2, 1, rows.length, propertyOrder.length).setValues(rows);
}
上記を実行すると対象のスプレッドシートにNotionExportというシートが作成され、そちらにNotionの情報が出力されます。※列の順番については自動取得では制御できないため別途実装が必要となります。
3‑2. pushSheetToNotion(スプレッド → Notion へページ追加)
async function pushSheetToNotion(): Promise<void> {
const props = PropertiesService.getScriptProperties();
const token = props.getProperty('NOTION_TOKEN')!;
const dbId = props.getProperty('NOTION_DB_ID')!;
const notionVersion = '2022-06-28';
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SheetToNotion');
if (!sheet) throw new Error('SheetToNotion がありません');
const [headers, ...values] = sheet.getDataRange().getValues();
// NotionのDBスキーマを取得
const dbMetaRes = UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${dbId}`, {
method: 'get',
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': notionVersion
}
});
const dbMeta = JSON.parse(dbMetaRes.getContentText());
const dbProperties = dbMeta.properties;
for (const row of values) {
const properties: Record<string, any> = {};
headers.forEach((h, i) => {
const value = row[i];
const prop = dbProperties[h];
if (!prop || value === '') return;
switch (prop.type) {
case 'title':
properties[h] = {
title: [{ text: { content: String(value) } }]
};
break;
case 'rich_text':
properties[h] = {
rich_text: [{ text: { content: String(value) } }]
};
break;
case 'date':
properties[h] = {
date: { start: new Date(value).toISOString().split('T')[0] }
};
break;
case 'number':
properties[h] = {
number: Number(value)
};
break;
case 'checkbox':
properties[h] = {
checkbox: value === true || value === 'TRUE' || value === '✓'
};
break;
case 'select':
case 'status':
properties[h] = {
[prop.type]: { name: String(value) }
};
break;
case 'multi_select':
properties[h] = {
multi_select: String(value)
.split(',')
.map((v: string) => ({ name: v.trim() }))
};
break;
case 'url':
properties[h] = {
url: String(value)
};
break;
default:
// 未対応型は無視
break;
}
});
const payload = {
parent: { database_id: dbId },
properties
};
UrlFetchApp.fetch('https://api.notion.com/v1/pages', {
method: 'post',
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': notionVersion,
'Content-Type': 'application/json'
},
payload: JSON.stringify(payload)
});
Utilities.sleep(500); // APIリミット対策
}
}
上記を実行する前にスプレッドシートにSheetToNotionというシートを作成してください。そちらの内容がNotionに登録されます。また列名はNotionと揃えるようにしてください。
3‑3. 関数の実行方法
main.tsに上記の関数を追加しnpx tsc したあとにclasp pushしてください。
成功すると以下の画像のように、GAS トリガーから呼び出せます。
また上記を実行する際、初回は以下のような承認画面が表示されるので権限を確認し追加してください。
3‑4. 双方向同期とトリガー設定
GAS のトリガー設定から fetchNotionToSheet/pushSheetToNotion をそれぞれ毎日や時間ごとに自動実行するよう設定すれば、Notion ↔ Sheets の双方向同期が実現します。重複対策として、ページIDのメタ情報保持や最終更新日時の比較ロジックを入れると堅牢です。
4. 運用上の注意点・ベストプラクティス
- API レート制限とスリープ処理
Notion API は1秒あたりの呼び出し制限があるため、繰り返し処理時には
Utilities.sleep()を入れる設計が重要です。 - 型安全とメンテナンス性
TypeScript による型定義(例:Notion API レスポンス型)を活用することで、コード改修やデータ拡張時も堅牢性が維持できます。
- バージョン管理・CI/CD
ソースコードは GitHub 等で管理し、GitHub Actions を使って
npm run build && clasp pushを自動化すれば、レビュー付きコードレビューや自動デプロイが可能になります 。 - 環境ごとの設定
.clasp.jsonにスクリプトIDを複数環境(開発/本番)用に管理し、必要時に切り替えてclasp pushする運用方法も活用できます。 - エラーハンドリング
UrlFetchApp.fetch()のレスポンスステータスチェック、try/catch 文によるエラーログの記録、およびユーザーへの通知(メール等)も導入しておくと安心です。
まとめ
clasp+TypeScript によるローカル開発環境をベースに、GAS を使って Notion データベースと Google スプレッドシートを 双方向に連携・同期させる構成は、以下のようなメリットをもたらします:
- Git によるバージョン管理、チーム開発、CI/CD自動デプロイ
- 秘密情報との分離・スクリプトプロパティによる環境変数管理
- 型安全なコードで安定性・可読性を担保
- 定期実行トリガーによる定常自動同期運用
- 重複防止、更新日時管理などによる堅牢な同期処理
ぜひ本記事のコード例をもとに、実際に構築・運用してみてください。
またNotionの運用やGASを使った業務効率化にお悩みの方はお問合せより気軽にご相談ください。

