GAS『TypeError: Cannot Read Property』の原因と対処法

GAS『TypeError: Cannot Read Property』の原因と対処法

Google Apps Scriptで「TypeError: Cannot read property ‘xxx’ of undefined/null」系のエラーは、未定義(undefined)やnullに対してプロパティやメソッドを呼び出したときに起きる。よくある原因と、すぐ試せる修正パターンを目的別にまとめた。

エラーの意味と代表メッセージ

TypeError: Cannot read property 'getRange' of null
TypeError: Cannot read property 'length' of undefined
TypeError: Cannot read properties of undefined (reading 'parameter')  // V8の文言

・「xxxがundefined/nullなのでxxx.yyyが読めない/呼べない」という意味。

発生条件の早見表

□ getSheetByName が null を返しているのに .getRange() を呼んだ
□ トリガー関数を手動実行して e(イベント引数)が undefined
□ PropertiesService から取得した値が null なのに .trim() などを呼んだ
□ 配列インデックス範囲外 / オブジェクトにそのキーが存在しない
□ Webhook/外部APIのJSON構造が想定と違う(パスのどこかがundefined)
□ 型の取り違え(数値に .split、文字列に .map など)

まずやること:どこが undefined/null かを特定

function trace(v, label='var') {
  console.log(label, {type: typeof v, isArray: Array.isArray(v), value: v});
}

// 例:落ちる直前でトレース
function example() {
  const ss = SpreadsheetApp.openById('...');
  const sh = ss.getSheetByName('Data');
  trace(sh, 'sheet');  // null ならここで確定
  sh.getRange(1,1).setValue('ok');
}

・例外箇所の直前で typeof / Array.isArray / 値を出力すると切り分けが速い。

原因1:シート/範囲が取得できず null(ID・シート名・A1指定)

// NG: シート名の誤字で sh は null
const sh = SpreadsheetApp.openById(ID).getSheetByName('Data '); // 末尾空白…

// OK: 取得を検証してから使う
function getSheetSafe(id, name) {
  const ss = SpreadsheetApp.openById(id);
  const sh = ss.getSheetByName(name);
  if (!sh) throw new Error(`シートが見つかりません: ${name}`);
  return sh;
}
const sh2 = getSheetSafe(ID, 'Data');
sh2.getRange('A1').setValue('hello');

・openByIdにURL全体を渡すミスも定番(IDのみ渡す)。

原因2:トリガーの e が undefined(手動実行/引数未設定)

// NG: 手動実行すると e は undefined
function onEdit(e) {
  const range = e.range; // TypeError
}

// OK: 直接実行時は安全に抜ける
function onEdit(e) {
  if (!e || !e.range) return;
  const range = e.range;
  // 本処理
}

// WebアプリdoPost/doGetも同様
function doPost(e) {
  if (!e || !e.postData || !e.postData.contents) return ContentService.createTextOutput('no body');
  const body = e.postData.contents;
  // 処理
}

・インストール型トリガー、ウェブアプリの実行主体・実行方法に注意。

原因3:PropertiesServiceの値が null(文字列メソッドの呼び出し)

// NG
const token = PropertiesService.getScriptProperties().getProperty('API_TOKEN');
const trimmed = token.trim(); // token が null なら TypeError

// OK
const token2 = (PropertiesService.getScriptProperties().getProperty('API_TOKEN') || '').trim();
if (!token2) throw new Error('API_TOKEN が未設定です');

・getPropertyは「未設定→null」を返す。デフォルト値で受けるか検証を入れる。

原因4:配列/オブジェクトの存在しない要素・キー参照

// NG: 範囲外アクセス
const arr = [1,2,3];
const x = arr[5].toString(); // arr[5] は undefined

// OK
const x2 = (arr[5] ?? '').toString(); // これでも "" に .toString() はOKだが、意図に応じて条件分岐
if (arr.length > 5 && arr[5] != null) {
  // 安全に処理
}

// オブジェクトキーも同様
const user = {profile: {name: 'A'}}; 
const city = user.profile && user.profile.address && user.profile.address.city; // 旧来
const city2 = user.profile?.address?.city ?? 'N/A'; // V8のオプショナルチェーン

・V8ランタイムでは ?. と ?? が使える(Apps Script標準)。

原因5:外部JSONの構造違い(パス途中で undefined)

// NG: 想定と違うレスポンスで落ちる
const res = UrlFetchApp.fetch(url);
const json = JSON.parse(res.getContentText());
const name = json.data.user.name.toUpperCase(); // dataやuserが無いとTypeError

// OK: パス存在を確認しつつデフォルト
const name2 = json?.data?.user?.name;
const upper = (typeof name2 === 'string') ? name2.toUpperCase() : 'UNKNOWN';

・APIのドキュメント更新や失敗時レスポンスで構造が変わることがある。

原因6:型の取り違え(数値/文字列/オブジェクト)

// NG
const v = 12345;
const parts = v.split('-'); // 数値に split はない → TypeError

// OK
const parts2 = String(v).split('-');

// NG
const s = 'abc';
const doubled = s.map(c => c+c); // 文字列に map はない → TypeError

// OK
const doubled2 = s.split('').map(c => c+c).join('');

・SpreadsheetのgetValuesは「2次元配列」を返す。1次元前提のコードも崩れやすい。

V8での安全な記法(?? / ?. / デフォルト / 分割代入)

// デフォルト値
const page = Number(e?.parameter?.page ?? 1);

// プロパティの分割代入と初期値
const { sheetName = 'Data', rangeA1 = 'A1' } = (config || {});

// Null合体で設定値を決める
const timeoutMs = Number(PropertiesService.getScriptProperties().getProperty('TIMEOUT_MS') ?? 5000);
[/code]

・「0」や空文字も許容したい場合は null/undefined を見分けられる ?? が便利。

防御的ユーティリティ(共通の落とし穴を回避)

const U = {
  req: (v, msg='値が必須です') => { if (v == null) throw new Error(msg); return v; },
  str: v => (v == null ? '' : String(v)),
  num: v => { const n = Number(v); if (Number.isNaN(n)) throw new Error(`数値に変換できません: ${v}`); return n; },
  getSheet(id, name) {
    const ss = SpreadsheetApp.openById(id);
    const sh = ss.getSheetByName(name);
    if (!sh) throw new Error(`シートが見つかりません: ${name}`);
    return sh;
  }
};

// 使用例
const sh = U.getSheet(ID, 'Data');
const token = U.req(PropertiesService.getScriptProperties().getProperty('API_TOKEN'), 'API_TOKEN未設定');

・入口で必須チェックを行うと、TypeErrorになる前に意味のあるメッセージで止められる。

よくあるNG→OK(スプレッドシート編)

// NG: 取得失敗を想定しない
const sh = SpreadsheetApp.openById(ID).getSheetByName(SHEET);
sh.getRange('A1').setValue('x'); // SHEETが違うとTypeError

// OK: ガードを入れる
const ss = SpreadsheetApp.openById(ID);
const sh2 = ss.getSheetByName(SHEET);
if (!sh2) throw new Error(`シート名が正しくありません: ${SHEET}`);
sh2.getRange('A1').setValue('x');

// NG: setValues なのに配列次元が合っていない(別エラーや想定外のundefined利用につながる)
sh2.getRange(1,1,3,2).setValues([['a','b','c']]); // 1x3 を 3x2 に書こうとして崩れる

// OK: サイズ一致を検証
const values = [['a','b'],['c','d'],['e','f']];
const range = sh2.getRange(1,1,values.length, values[0].length);
range.setValues(values);

テスト用:意図的に再現→修正の通し例

// 再現
function fail_case() {
  const sh = SpreadsheetApp.openById('ID').getSheetByName('WrongName'); // null
  sh.getRange('A1').setValue('x'); // TypeError
}

// 修正
function fix_case() {
  const ss = SpreadsheetApp.openById('ID');
  const name = 'Data';
  const sh = ss.getSheetByName(name);
  if (!sh) throw new Error(`シート '${name}' が見つかりません`);
  sh.getRange('A1').setValue('ok');
}

Webhook/フォームのイベント引数を安全に扱う

function doPost(e) {
  const raw = e?.postData?.contents ?? '';
  if (!raw) return ContentService.createTextOutput('empty');
  let json;
  try { json = JSON.parse(raw); } catch (err) { return ContentService.createTextOutput('bad json'); }
  const userId = json?.user?.id ?? null;
  if (!userId) return ContentService.createTextOutput('no user id');
  // 正常処理…
  return ContentService.createTextOutput('ok');
}

・イベント引数の有無、JSONの妥当性、必須キーの存在を段階的に確認。

チェックリスト(上から順に)

□ 例外行の直前で typeof / 値をログに出したか
□ シート取得やA1範囲は正しいか(名前の空白・IDの取り違えなし)
□ トリガー関数を手動実行していないか(e を必ず検査)
□ PropertiesService 等の戻り値が null でないか(デフォルト値/検証)
□ 配列・オブジェクトのキー有無、インデックス範囲を確認したか
□ 外部JSONの構造変化を想定して ?. と ?? で防御したか
□ setValues のサイズを範囲と合わせたか(2次元配列)

ひな形:落ちない土台(安全ガード込み)

function safeMain(e) {
  try {
    const ID = '...';
    const SHEET = 'Data';
    const sh = SpreadsheetApp.openById(ID).getSheetByName(SHEET);
    if (!sh) throw new Error(`シートが見つかりません: ${SHEET}`);

    const page = Number(e?.parameter?.page ?? 1);
    if (!Number.isFinite(page) || page < 1) throw new Error('pageが不正');

    sh.getRange('A1').setValue(`page=${page}`);
  } catch (err) {
    console.error({
      name: err.name, message: err.message, stack: err.stack
    });
    throw err; // ログを残して再送出
  }
}

・null/undefinedの可能性がある箇所にガードを添えると、TypeErrorの大半は回避できる。