@kintone/dts-genで作られたJavaScript API用の型を、REST APIの型にも流用する

kintoneでJavaScriptカスタマイズされているみなさま、TypeScriptを利用していますか？
JavaScriptに加え、「型」という概念や作法も覚えていかないといけない分もちろん学習コストは必要ですが、型で守られた堅牢なコードを書きたい、コードを今後も書き続けたい人にとってはメリットのほうが遥かにうわまります。

具体的なメリットやkintoneでの導入の仕方などについてはぜひ下記を参考にしてみてください。

[目指せ！JavaScriptカスタマイズ中級者（５） 〜TypeScript導入編〜](https://developer.cybozu.io/hc/ja/articles/900005568163)

---

TypeScriptをkintoneカスタマイズに既に使っている人向けの記事にはなりますが、
ここでは表題の通り「@kintone/dts-genで作られたJavaScript API用の型を、REST APIの型にも流用する」ということを試してみたので、やったことを残しておきます。

# 大前提: JavaScript API と REST APIでレコードの型がそもそも違う
日頃JSカスタマイズをされている方じゃないとあまりピンとこないかもしれませんが...
一覧画面表示イベントなど**[JavaScript API](https://developer.cybozu.io/hc/ja/articles/360000361686)**で利用できるレコードと、 `kintone.api()` などで **[REST API](https://developer.cybozu.io/hc/ja/articles/360000313406)**で取得できるレコードの内容は実は違います。

例えば、JavaScript　APIでは[フィールドの編集可・不可](https://developer.cybozu.io/hc/ja/articles/201941984#step5)を設定するためには、
`record['文字列_0'].disabled = true;` などのように、
レコードオブジェクトの各フィールドプロパティの `disabled` のフラグを編集する必要があります。
これがREST APIで扱うレコードオブジェクトでは設定できません。


# 問題

`@kintone/dts-gen` では JavaScript API用のレコードの型は生成できるのですが、REST API用の型の生成には対応していません。[^1]
ですので、REST API用にレコードの型を定義するには、 `@kintone/rest-api-client` を使って[手動で定義を書かないといけません。](https://github.com/kintone/js-sdk/blob/master/packages/rest-api-client/src/KintoneFields/exportTypes/__checks__/usecases/field.ts)[^2]

```ts
type MyAppRecord = {
  $id: KintoneRecordField.ID;
  CreatedBy: KintoneRecordField.Creator;
  EmployeeNo: KintoneRecordField.Number;
  Authorizer: KintoneRecordField.UserSelect;
  Title: KintoneRecordField.SingleLineText;
  Details: KintoneRecordField.Subtable<{
    Date: KintoneRecordField.Date;
    Destination: KintoneRecordField.SingleLineText;
    ModeOfTransportation: KintoneRecordField.Dropdown;
    Cost: KintoneRecordField.Number;
  }>;
  TotalExpenses: KintoneRecordField.Number;
  Notes: KintoneRecordField.MultiLineText;
};
```

[^1]: そもそもJavaScript API向けに作られたものなので
[^2]: 公式で出されているツールでは。公開されているサードパーティのものがあるかもしれません。

# 解決方法

そこで、今回は`@kintone/dts-gen`でJavaScript API用に作られた型をうまくREST APIに流用できないか、というアプローチです。
（注意点としては、本来的には、あんまりよくないアプローチだとは思います...これやるくらいならREST API用に型を出力できるツールを作ったほうが本当はいいですが、今回は比較的簡単にできそう、ということで試してみました）


# 詳細
## @kintone/dts-genでJavaScript API用の型を生成する
上記記事に詳しくは説明されていますが、　[@kintone/dts-gen](https://github.com/kintone/js-sdk/tree/master/packages/dts-gen) というライブラリを使うことで、JavaScript　APIで扱うためのレコードの型定義をアプリから取得できます。

取得例

```
npx @kintone/dts-gen --base-url https://kintoneのドメイン.cybozu.com -u ユーザー名 -p パスワード --app-id アプリID --type-name Quote --namespace KintoneTypes -o src/types/Quote.d.ts
```

コマンドが成功すると、下記のような型定義ファイルが生成されます。


```ts
// src/types/Quote.d.ts
declare namespace KintoneTypes {
  interface Quote {
    No: kintone.fieldTypes.SingleLineText;
    文字列__1行_: kintone.fieldTypes.SingleLineText;
    文字列__複数行_: kintone.fieldTypes.MultiLineText;
    日付: kintone.fieldTypes.Date;
    合計金額: kintone.fieldTypes.Calc;
    見積明細: {
      type: "SUBTABLE";
      value: {
        id: string;
        value: {
          単価: kintone.fieldTypes.Number;
          数量: kintone.fieldTypes.Number;
          型番: kintone.fieldTypes.SingleLineText;
          商品名: kintone.fieldTypes.SingleLineText;
          小計: kintone.fieldTypes.Calc;
        };
      }[];
    };
  }
  interface SavedQuote extends Quote {
    $id: kintone.fieldTypes.Id;
    $revision: kintone.fieldTypes.Revision;
    更新者: kintone.fieldTypes.Modifier;
    作成者: kintone.fieldTypes.Creator;
    レコード番号: kintone.fieldTypes.RecordNumber;
    更新日時: kintone.fieldTypes.UpdatedTime;
    作成日時: kintone.fieldTypes.CreatedTime;
  }
}
```

これを用いることで一覧画面表示イベントなどで渡ってくるレコードの情報に安全にアクセスできます。

```ts
kintone.events.on(events, async (event) => {
  const record = event.record as KintoneTypes.Quote;
  console.log(record.合計金額.value);
})
```

## REST APIで使えるようにマッピングする
例えば、 1行文字列フィールドは、
`@kintone/dts-gen` (JavaScript API) では `kintone.fieldTypes.SingleLineText` と表現され、 `@kintone/rest-api-client` (REST API) では `KintoneRecordField.SingleLineText` と表現されます。
これを1個1個、Conditional Typesを用いて変換していきます。

```ts
// rest_types.d.ts
import { KintoneRecordField } from '@kintone/rest-api-client';

// 通常のレコードがもつフィールド情報
type BasicFieldTypes<T> = T extends kintone.fieldTypes.Id
  ? KintoneRecordField.ID
  : T extends kintone.fieldTypes.Revision
  ? KintoneRecordField.Revision
  : T extends kintone.fieldTypes.Modifier
  ? KintoneRecordField.Modifier
  : T extends kintone.fieldTypes.Creator
  ? KintoneRecordField.Creator
  : T extends kintone.fieldTypes.CreatedTime
  ? KintoneRecordField.CreatedTime
  : T extends kintone.fieldTypes.UpdatedTime
  ? KintoneRecordField.UpdatedTime
  : T extends kintone.fieldTypes.GroupSelect
  ? KintoneRecordField.GroupSelect
  : InTableFields<T>;

// テーブルの中でしか扱えないフィールド情報
type InTableFields<T> = T extends kintone.fieldTypes.SingleLineText
  ? KintoneRecordField.SingleLineText
  : T extends kintone.fieldTypes.MultiLineText
  ? KintoneRecordField.MultiLineText
  : T extends kintone.fieldTypes.Number
  ? KintoneRecordField.Number
  : T extends kintone.fieldTypes.Date
  ? KintoneRecordField.Date
  : T extends kintone.fieldTypes.DateTime
  ? KintoneRecordField.DateTime
  : T extends kintone.fieldTypes.Time
  ? KintoneRecordField.Time
  : T extends kintone.fieldTypes.DropDown
  ? KintoneRecordField.Dropdown
  : T extends kintone.fieldTypes.CheckBox
  ? KintoneRecordField.CheckBox
  : T extends kintone.fieldTypes.MultiSelect
  ? KintoneRecordField.MultiSelect
  : T extends kintone.fieldTypes.Calc
  ? KintoneRecordField.Calc
  : T extends kintone.fieldTypes.File
  ? KintoneRecordField.File
  : T extends kintone.fieldTypes.OrganizationSelect
  ? KintoneRecordField.OrganizationSelect
  : T extends kintone.fieldTypes.RadioButton
  ? KintoneRecordField.RadioButton
  : T extends kintone.fieldTypes.RichText
  ? KintoneRecordField.RichText
  : T extends kintone.fieldTypes.UserSelect
  ? KintoneRecordField.UserSelect
  : T extends kintone.fieldTypes.Link
  ? KintoneRecordField.Link
  : never;

type Subtable = {
  type: 'SUBTABLE';
  value: {
    id: string;
    value: {
      [key: string]: unknown;
    };
  }[];
};

export type RestRecord<T> = {
  [K in keyof T]: T[K] extends Subtable
    ? KintoneRecordField.Subtable<
        { // テーブルの中は階層が深いのでちょっとややこしいがこれで表現できる
          [R in keyof T[K]['value'][number]['value']]: InTableFields<T[K]['value'][number]['value'][R]>;
        }
      >
    : BasicFieldTypes<T[K]>;
};
```

この型定義ファイルを読み込ませたあと、下記のように`@kintone/dts-gen` で作られた型情報を引数に渡すと、うまい具合にREST API用の型として使えます。
注意点としては、`@kintone/dts-gen`では保存前のレコードの型と保存後のレコードの型の両方が作られますので、保存後のものを利用するようにしましょう。（REST APIには保存前のレコードという概念がない）

```ts
const record: RestRecord<SavedQuote> = {/* 型が効く */};
```

邪道かもですが、一応こういうふうにすればJavaScriptAPIの型→REST APIの型というふうに、型変換できる、という実験でした。
なにか動かないとかアレばコメントいただければと思います！