[TypeScript]仕様を守らない者に地獄送りの裁きを！ ～ ジハード ～  Analyticsのgtagの型を片っ端から固めてみた


※ TypeScriptでオーバーロードを使わずに、異なる引数を持つ関数を作る疑似オーバロードは**関数宣言に統合**を確認してください
※ あんまり活用が進まない**Exclude**による型フィルターの使い方も同じ場所にあります

# それは地獄の蓋を開くきっかけだった

　現在運用中の[技術情報掲載サイト](https://croud.jp/)をPHPからNode.js(TypeScript)に移植している途中で、フロントエンドの方もかなりの部分を作り直している。そこでAnalytics関連の組み込みを行おうとしたら、npmにgtagの適切な@typesな定義が見当たらなかった。探し方が悪いだけなのかもしれないが、仕様を確認して自分で作ってみることにした。それが地獄の蓋を開くことになるとも知らずに。

## １．Analyticsの仕様を確認

　実際に必要とするパラメータは極わずかなのだが、仕様を確認してみると地獄のパラメータの多さである。情報が分散していて、結局のところの仕様を抽出するのが非常にやりにくかった。後半に関しては、もはやいらないのではとも思ったが、こうなったら意地である。

https://developers.google.com/analytics/devguides/collection/gtagjs/pages?hl=ja
https://developers.google.com/analytics/devguides/collection/gtagjs/events?hl=ja
https://developers.google.com/analytics/devguides/collection/gtagjs/screens?hl=ja
https://developers.google.com/analytics/devguides/collection/gtagjs/user-timings?hl=ja
https://developers.google.com/analytics/devguides/collection/gtagjs/exceptions?hl=ja
https://developers.google.com/gtagjs/reference/api?hl=ja
https://developers.google.com/gtagjs/reference/event?hl=ja
https://developers.google.com/gtagjs/reference/parameter?hl=ja


　基本的には以下の三種類のコマンドがある。

- config congigには専用パラメータと、eventパラメータ全てを初期設定しておく機能がある
- set ユーザが設定しておきたいデータで自由使用 
- event 大量の推奨パラメータとカスタム型、コイツが全ての地獄仕様の現況

　eventに関しては、定義されているパラメータが多すぎて気が狂いそうになった。こいつら全てをTypeScriptの型チェックに引っかけて、仕様から逸脱したら赤線を引っ張ってエラーにしてやるのだ！

## ２．仕様に基づいて、ひたすらinterface化

　TypeScriptでは、これが非常に面倒な作業である。npmに上がっているものなら話は早いのだが、無かった瞬間に地獄は始まるのだ。

### config用

```.ts
/**
 *config用コントロールパラメータ
 *
 * @interface ControlParameter
 */
declare interface ControlParameter {
  groups?: string | string[];
  send_to?: string | string[];
  event_callback?: () => void;
  event_timeout?: number;
}
/**
 *config用ページビューパラメータ
 *
 * @interface PageViewParameter
 */
declare interface PageViewParameter {
  send_page_view?: boolean;
  groups?: string;
  page_title?: string;
  page_location?: string;
  page_path?: string;
}

```

### event用
```.ts
/**
 *event用推奨パラメータ
 *
 * @interface DefaultEventParameter
 */
declare interface DefaultEventParameter {
  checkout_option?: string;
  checkout_step?: number;
  content_id?: string;
  content_type?: string;
  coupon?: string;
  currency?: string;
  description?: string;
  fatal?: boolean;
  items?: {
    brand: string;
    category: string;
    creative_name: string;
    creative_slot: string;
    id: string;
    location_id: string;
    name: string;
    price: number;
    quantity: string;
  }[];
  method?: string;
  name?: string;
  promotions?: {
    creative_name: string;
    creative_slot: string;
    id: string;
    name: string;
  }[];
  screen_name?: string;
  search_term?: string;
  shipping?: number;
  tax?: number;
  transaction_id?: string;
  value?: number;
  app_name?: string;
  app_id?: string;
  app_version?: string;
  app_installer_id?: string;
}

/**
 *event用推奨イベント
 *
 * @interface DefaultEvent
 */
declare interface DefaultEvent {
  add_payment_info?: undefined;
  add_to_cart?: {
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    items?: DefaultEventParameter["items"];
  };
  add_to_wishlist?: {
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    items?: DefaultEventParameter["items"];
  };
  begin_checkout?: {
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    items?: DefaultEventParameter["items"];
    coupon?: DefaultEventParameter["coupon"];
  };
  checkout_progress?: {
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    items?: DefaultEventParameter["items"];
    coupon?: DefaultEventParameter["coupon"];
    checkout_step?: DefaultEventParameter["checkout_step"];
    checkout_option?: DefaultEventParameter["checkout_option"];
  };
  exception?: {
    description?: DefaultEventParameter["description"];
    fatal?: DefaultEventParameter["fatal"];
  };
  generate_lead?: {
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    transaction_id?: DefaultEventParameter["transaction_id"];
  };
  login?: {
    method?: DefaultEventParameter["method"];
  };
  page_view?: undefined;
  purchase?: {
    transaction_id?: DefaultEventParameter["transaction_id"];
    value?: DefaultEventParameter["value"];
    tax?: DefaultEventParameter["tax"];
    shipping?: DefaultEventParameter["shipping"];
    items?: DefaultEventParameter["items"];
    coupon?: DefaultEventParameter["coupon"];
  };
  refund?: {
    transaction_id?: DefaultEventParameter["transaction_id"];
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    tax?: DefaultEventParameter["tax"];
    shipping?: DefaultEventParameter["shipping"];
    items?: DefaultEventParameter["items"];
  };
  remove_from_cart?: {
    value?: DefaultEventParameter["value"];
    currency?: DefaultEventParameter["currency"];
    items?: DefaultEventParameter["items"];
  };
  screen_view?: {
    screen_name?: DefaultEventParameter["screen_name"];
    app_name?: DefaultEventParameter["app_name"];
    app_id?: DefaultEventParameter["app_id"];
    app_version?: DefaultEventParameter["app_version"];
    app_installer_id?: DefaultEventParameter["app_installer_id"];
  };
  search?: {
    search_term?: DefaultEventParameter["search_term"];
  };
  select_content?: {
    items?: DefaultEventParameter["items"];
    promotions?: DefaultEventParameter["promotions"];
    content_type?: DefaultEventParameter["content_type"];
    content_id?: DefaultEventParameter["content_id"];
  };
  set_checkout_option?: {
    checkout_step?: DefaultEventParameter["checkout_step"];
    checkout_option?: DefaultEventParameter["checkout_option"];
  };
  share?: {
    method?: DefaultEventParameter["method"];
    content_type?: DefaultEventParameter["content_type"];
    content_id?: DefaultEventParameter["content_id"];
  };
  sign_up?: {
    method?: DefaultEventParameter["method"];
  };
  timing_complete?: {
    name?: DefaultEventParameter["name"];
    value?: DefaultEventParameter["value"];
  };
  view_item?: {
    items?: DefaultEventParameter["items"];
  };
  view_item_list?: {
    items?: DefaultEventParameter["items"];
  };
  view_promotion?: {
    promotions?: DefaultEventParameter["promotions"];
  };
  view_search_results?: {
    search_term?: DefaultEventParameter["search_term"];
  };
}


/**
 *event用通常パラメータ
 *
 * @interface EventParameter
 */
declare interface EventParameter {
  event_category?: string;
  event_label?: string;
  value?: number;
  non_interaction?: boolean;
}
```

### 関数宣言に統合

　引数や型が異なる関数を宣言する場合、TypeScriptではオーバロードを使うのが一般的っぽいが、実は必要ない。可変引数と共用体型を使えば、一つの関数の宣言中に全てを記述することが可能だ。


```.ts
/**
 *gtag定義
 *
 * @template K
 * @param {(...["config", string, (ControlParameter|PageViewParameter|DefaultEventParameter)?]
 *     | ["set", { [key: string]: string }]
 *     | ["event", K, (DefaultEvent[K] & EventParameter)]
 *     | ["event", Exclude<string, K>, EventParameter?])} params
 */
declare function gtag<K extends keyof DefaultEvent>(
  ...params:
    | ["config", string, (ControlParameter|PageViewParameter|DefaultEventParameter)?]
    | ["set", { [key: string]: string }]
    | ["event", K, (DefaultEvent[K] & EventParameter)]
    | ["event", Exclude<string, K>, EventParameter?]
): void;
```

## ３．使ってみる

　Analyticsのサンプルで使われている以下のコードが全て通ることを確認した。もちろん関係ないパラメータを設定するとエラーになるし、数値であるべき場所に文字列を放り込むとやっぱりエラーになる。

```.ts
gtag("config", "GA_MEASUREMENT_ID", {
  page_title: "homepage",
  page_path: "/home"
});
gtag("config", "GA_MEASUREMENT_ID_1");
gtag("config", "GA_MEASUREMENT_ID_2");

gtag("event", "xyz");

gtag("event", "aaa", {
  event_category: "bbb",
  event_label: "ccc"
});

gtag("event", "login", { method: "Google" });

gtag("event", "video_auto_play_start", {
  event_label: "My promotional video",
  event_category: "video_auto_play",
  non_interaction: true
});

gtag("event", "screen_view", {
  app_name: "myAppName",
  screen_name: "Home"
});
gtag('config', 'GA_MEASUREMENT_ID', { 'app_name': 'myAppName' });
gtag('event', 'screen_view', { 'screen_name': 'Home'});


gtag('event', 'timing_complete', {
  'name' : 'load',
  'value' : 3549,
  'event_category' : 'JS Dependencies'
});
gtag('event', 'exception', {
  'description': 'error_description',
  'fatal': false   
});
```

##４．使うのこれ？

　たぶん使わない。configの一部しか使わない。event使わない。とりあえず出来たのでnpmに放り込んでおくべきだろうか？