※ TypeScriptでオーバーロードを使わずに、異なる引数を持つ関数を作る疑似オーバロードは関数宣言に統合を確認してください
※ あんまり活用が進まないExcludeによる型フィルターの使い方も同じ場所にあります
それは地獄の蓋を開くきっかけだった
現在運用中の技術情報掲載サイトをPHPからNode.js(TypeScript)に移植している途中で、フロントエンドの方もかなりの部分を作り直している。そこでAnalytics関連の組み込みを行おうとしたら、npmにgtagの適切な@typesな定義が見当たらなかった。探し方が悪いだけなのかもしれないが、仕様を確認して自分で作ってみることにした。それが地獄の蓋を開くことになるとも知らずに。
1.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の型チェックに引っかけて、仕様から逸脱したら赤線を引っ張ってエラーにしてやるのだ!
2.仕様に基づいて、ひたすらinterface化
TypeScriptでは、これが非常に面倒な作業である。npmに上がっているものなら話は早いのだが、無かった瞬間に地獄は始まるのだ。
config用
/**
*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用
/**
*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ではオーバロードを使うのが一般的っぽいが、実は必要ない。可変引数と共用体型を使えば、一つの関数の宣言中に全てを記述することが可能だ。
/**
*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;
3.使ってみる
Analyticsのサンプルで使われている以下のコードが全て通ることを確認した。もちろん関係ないパラメータを設定するとエラーになるし、数値であるべき場所に文字列を放り込むとやっぱりエラーになる。
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
});
4.使うのこれ?
たぶん使わない。configの一部しか使わない。event使わない。とりあえず出来たのでnpmに放り込んでおくべきだろうか?