Typescriptを利用してスプレッドシートを操作する【Google sheets APIの利用】

普段最も扱う言語は？と問われれば、『GAS』と言ってきたのですが利便性を理解しつつも世の流れに乗っていないという自覚もあり、どこかでTypescriptをしっかり学ばなければと感じていました。
これからの仕事をしていく上で、Typescriptをかけるということはスタンダード技術であると思っているので、そこに至るべくできることをし始めたのであります。

## GAS開発の概要
Google apps script（GAS）を利用した開発はもっぱらスプレッドシートの操作が主でした。特定のファイルからデータを引っ張り、加工して別ファイルに貼り付ける。
さらにはその挙動を自動化する。
といった具合に開発をしており、割と（個人的には）いい感じでシステムが構築できていました。
時折Googleカレンダーからのデータ取得もしていましたが、その辺もGASでまとめてできていたのもよかったです。

ただ、GitHubとの連携を考えたときに、Chromeの拡張機能だけで運用するのはちょっと寂しく、ブランチ管理はできるもののissue管理には届いていない状況だったので、なんだか活かしきれてないなぁと感じていたのでした。

## Typescriptを利用した開発：claspの利用
『GAS　ローカル環境　開発』とかで調べると、VScodeを使ってtypescriptで開発し、Google謹製のclaspを使ってpushすれば簡単だよという記事が見つかったので試してみました。
確かにVScode（僕の場合はCursor）を使って開発するのってやりやすいのでそれ自体は良かったし、clasp使えば手軽にpushできて楽じゃーんとも思っていたのですが、欠点も見つかってきました。

### VScode（Typescript）とclasp利用での欠点
やってみて感じたこととしては、僕がメインで行なっているスプレッドシートの操作に関する記述でした。
具体的には普段GAS上では`SpreadsheetApp`クラスを利用してプログラミングすることが非常に多かったわけですが、VScode上では（Javascriptへコンパイル後）動作しないんですね…（そらそうか）
Typescript開発をする中でスプレッドシートへのアクセス、操作を考えるとGoogle sheets APIをはじめとしたGCPの利用をすることになるわけです。
そうなると、Typescript→Javascriptへのコンパイル後に動作の確認ができるので、あれ？GASエディタにいく必要なくね？ってなってきたんです。
でもその事実をうまく言語化できてなくて堂々巡りというかいろいろやってみてたんですが、なんか無駄だなと。

## 結局たどり着いたTypescriptを利用したスプレッドシート操作
結局のところ、GCPでGoogle Sheets APIを有効化して、VScode（Cursor）上で開発し、ターミナルからGithub連携させていくことで落ち着きました（今の所は）。
ただ、GASでやっていた『クラス操作から目的の挙動を作る』ということとTypescriptで行う『APIを叩くことで帰ってきたレスポンスに対して加工を行い目的の挙動を作る』といった部分で頭が切り替えるまでに時間がかかりました。

加えて、TypescriptでGASの代わりに開発できるよって記事を見つけて読んでみても、基本的にはclaspによるpush操作までで、知りたいのはそっから先やねん…！って思うことも多々あり。ChatGPTさんにもいろいろと聞きながら学習中なわけです。

## TypescriptでGoogle sheets APIを叩いてスプレッドシートを操作する
### 事前準備（GCP側）
1. Google cloud platformにいってAPIを有効化する
1. 認証情報作ってできたメールアドレスに対して当該スプレッドシートへのアクセス権限を付与
1. 認証情報作ってできた秘密鍵のJSONファイルをダウンロード

### 事前準備（エディタ編)
1. ローカル環境にディレクトリを作る
1. 必要なものをインストール
    ルートディレクトリに入って
    `npm install --save-dev typescript`
    `npm install googleapis google-auth-library fs dotenv`
1. 上記秘密鍵のJSONファイルをルートディレクトリにアップロード
1. .envファイルや.gitignoreファイルなどの作成

これで開発を始めます。
※ 現在のディレクトリ構造
```
root
    |- node_modules/
    |- src/
        |- 編集するファイル
     - .env
     - .gitignore
     - package-lock.json
     - package.json
     - tsconfig.json
     - 秘密鍵JSON
```
gitへpushするときには秘密鍵やenvファイルを除外するように注意してください。


### スプレッドシートへのアクセス
APIを叩いてスプレッドシートにアクセスするため必要となるのは
- アクセスするスプレッドシートのID
- 秘密鍵のJSONファイル
になります。
たとえば任意のスプレッドシートにアクセスし、すべてのシートを取得した上で特定の条件に合致（今回は『前月の情報が入っているシート』を想定）するシートのみ返すプログラムは以下のようになります。

```typescript:src/getSheets.ts
import { google } from "googleapis";
import { JWT } from "google-auth-library";
import * as fs from "fs";
import "dotenv/config";

import { SERVICE_ACCOUNT_FILE, SPREADSHEET_ID } from "./main";

export async function getTargetSheets() {
  // サービスアカウント認証のセットアップ
    const credentials = JSON.parse(fs.readFileSync(SERVICE_ACCOUNT_FILE, "utf8"));
    const auth = new JWT({
        email: credentials.client_email,
        key: credentials.private_key,
        scopes: ["https://www.googleapis.com/auth/spreadsheets"],
    });

  // Google Sheets APIの初期化
    const sheets = google.sheets({ version: "v4", auth });

    try {
    // スプレッドシートのデータを取得
    const res = await sheets.spreadsheets.get({
        spreadsheetId: SPREADSHEET_ID,
    });

    let targetSheets: string[] = [];

    res.data.sheets?.forEach((sheet) => {
      // 前月のシートのみ取得する
        const today: Date = new Date();
        let year: string = today.getFullYear().toString().slice(2);
        let month: string = today.getMonth().toString();
        let targetMonth: string = year + "年" + month + "月";
        if (month === "0") {
            targetMonth = "24年12月";
        }
        if (sheet.properties?.title && sheet.properties.title.toString().match(targetMonth) !== null) {
            targetSheets.push(sheet.properties.title);
        }
    });
    // console.log(targetSheets);
    return targetSheets;


    } catch (err) {
        console.error("エラー:", err);
    }
}
```

さらにシート全体の情報を取得するのは以下のプログラムになります。
上で返ってきたシート情報に対して、とりあえず配列の一つ目についてセルの値を取るようにしています。
```typescript:src/main.ts
// 外部モジュール
import { google } from 'googleapis';
import { JWT } from 'google-auth-library';
import * as fs from 'fs';
import "dotenv/config";

// 自作ファイルからのインポート
import { getTargetSheets } from './getSheetInfo';

// サービスアカウントの秘密鍵ファイルパス
export const SERVICE_ACCOUNT_FILE: string = "秘密鍵JSONファイルへのパス";

// スプレッドシートIDと範囲
export const SPREADSHEET_ID = process.env.RESERVATION_FILE_ID;

async function getSpreadsheetData() {
    const targetSheets = await getTargetSheets();
    if (!targetSheets || targetSheets.length === 0) {
        throw new Error('No target sheets found');
    }

    // サービスアカウント認証のセットアップ
    const credentials = JSON.parse(fs.readFileSync(SERVICE_ACCOUNT_FILE, 'utf8'));
    const auth = new JWT({
        email: credentials.client_email,
        key: credentials.private_key,
        scopes: ['https://www.googleapis.com/auth/spreadsheets'],
    });

    // Google Sheets APIの初期化
    const sheets = google.sheets({ version: 'v4', auth });

    try {
        console.log(targetSheets)
        // スプレッドシートのデータを取得
        const response = await sheets.spreadsheets.values.get({
            spreadsheetId: SPREADSHEET_ID,
            range: targetSheets[0]
        });

        // データを表示
        const rows = response.data.values;
        if (rows && rows.length > 0) {
            console.log('データ取得成功:');
            rows.forEach((row) => {
                console.log(row);
            });
        } else {
            console.log('データが見つかりませんでした。');
        }
    } catch (err) {
        console.error('エラー:', err);
    }
}

getSpreadsheetData();

```

あとはシートごとにデータの取得を回したり、整形していけばいいので、この辺の備忘録はいずれまたアウトプットします。


いろいろ開発を進めていけば、ターミナルからリモートリポジトリに対してpushするわけですが、ブランチ切ってコミット時にissue番号つけておけば、issueに応じたプルリクにもつながっていきます。
参考：https://qiita.com/kuma_radcof/items/933588e38fbefc0f9e65