iframe から、丸型の共有ボタンと「いいね」へ。Promari SNS Share を公開しました。
作成: / 公開: / 内容更新:
執筆:Takaomi Murasaki / 掲載・運営:プロマリ
丸い共有ボタンの設計を、用語のポップアップと身近なたとえでたどります。表示・保存・計測・配信の内容を省略せず、四層の責任と設計判断を詳しく解説。全5ページ、連載予定は16章。
ボタンを増やすたびに、全部直したくない
WordPressの外へも持ち出せる部品にする
それでは、ここからはボタンの中をもう少し詳しく見ていきましょう。今回のプラグインは <promari-sns-share> というWeb Componentです。自分で付けた名前のHTML要素を、ブラウザへ登録して使います。せっかく作るのなら、WordPressだけでなく普通のHTMLにも置いて使いたい。そう思って、この形を選びました。丸型の表示は variant="circle" で指定します。内部はShadow DOMで描くため、テーマ側のボタン用CSSと不用意にぶつかりにくくなっています。
ソースは、共有URLや設定の判断を行うdomain、操作の流れを組むapplication、ブラウザ機能を扱うinfrastructure、描画を担うpresentationに分けています。PHP側には共有サービスの契約と実装があり、設定生成器がWeb Component用のサービス情報を取り出します。サービスを増やす際は実装と登録・設定を更新し、生成物とテストの整合を確認します。
この分担を、飲食店の仕事に置き換えてみます。domainは「何を注文でき、どう組み合わせるか」という決まり、applicationは注文から提供までの手順、infrastructureは調理機器や配達手段、presentationはメニューと受け渡し口です。設備を替えたとき、料理の組み合わせの決まりまで毎回直すのは避けたいですよね。コードでも、何が変わったときに直す部分なのかを基準にします。
たとえば共有先のURL形式が変わったら、そのサービスのURL生成と期待値を見直します。コピーに使うブラウザ機能が変わったら、外側の接続実装を調べます。丸型ボタンの余白を変えるなら描画とスタイルを調べます。いいねの保存先を変えるなら、サイト側の接続が同じ確定状態を返せるか確認します。「どこから直し始めるか」が要求から分かることが、分ける利点です。
| 変更したいこと | 最初に調べる場所 | 保ちたい約束 |
|---|---|---|
| 共有先やURLの選び方 | domainとサービス定義 | 同じ入力に対する判断とURLの意味 |
| コピー成功・失敗後の手順 | application | 結果を待って正しい案内へ進む順序 |
| ブラウザ機能の呼び方 | infrastructure | 必要な能力と失敗の返し方 |
| 色・件数・操作中の表示 | presentation | 公開属性・メソッド・イベントの意味 |
| いいねの保存先 | サイト側の接続と保存API | 確定したlikedと件数を部品へ返すこと |
- 表示
- プラグインが共有ボタン、追加メニュー、いいね、件数、操作結果を描画します。
- 保存
- サイト側がいいねの要求を受け取り、サーバーで確定した状態を返します。
- 計測
- 共有イベントをサイト側で受け取り、同意設定に従ってアクセス解析へ渡します。
Web Componentsは、独自のHTML要素をブラウザへ登録して再利用するための仕組みです。Custom Elementsが要素の名前とライフサイクルを扱い、Shadow DOMが内部のDOMとスタイルの範囲を分けます。Reactなど特定のUIフレームワークをホスト側へ要求せず、同じ要素を静的HTMLにもWordPressにも配置できることが採用理由でした。 まず、依存という言葉の意味を確認しましょう。設計ポイント:フレームワークに依存しない部品
Shadow DOMはセキュリティの隔離環境ではありません。同じページのJavaScriptからのアクセスを完全に遮断する仕組みでもなく、CSSカスタムプロパティなど継承する値もあります。テーマとの偶発的なスタイル衝突を減らす境界として使います。また、独自要素の登録前にメソッドを呼べば利用できないため、連携コードではcustomElements.whenDefinedで登録の完了を待ちます。読み込み順を偶然に任せないためです。
Custom Elementで気になるのは、HTMLとJavaScriptの準備が同時ではないところです。ホスト側がsetLikeState()を呼ぶ時点で、まだ要素がアップグレードされていない可能性があります。そこでcustomElements.whenDefined()を待ちます。ただし、登録完了は保存APIの初期値取得まで保証しません。要素を呼べる状態と、操作を許可できる状態は別。この二段階を一緒にすると、回線が遅いときだけ再現する不具合になりやすいんです。
待つ対象を二つに分けると、読み込みの順番を整理できます。まず部品が登録され、メソッドを使えるようになるのを待ちます。次にサイト側が保存APIから初期状態を受け取り、その値を渡します。途中で片方だけ失敗したら、未取得を0件へ読み替えず、操作を待たせるか再取得へ案内します。部品が登録されない場合まで無期限に待つのではなく、読み込み失敗を扱う方針も必要です。
ライフサイクルは、部品が生まれ、画面につながり、外されるまでの節目です。画面につながるたびにイベントを登録し、外すときに後始末をしなければ、同じ操作へ何度も反応する原因になります。SPAのようにページ全体を読み直さず画面を切り替える環境では、特に見落としたくないところです。一度表示できたかだけでなく、「外して、もう一度置いたらどうなるか」まで試します。
Shadow DOMで内部のCSSを閉じるなら、外へ何を公開するかもセットで考えます。CSSカスタムプロパティや::partは調整用の入口になりますが、公開すれば利用側が依存する契約にもなります。「見た目だけだから自由に変えていい」とは言いにくくなるわけです。内部のDOM構造をそのままAPIにするより、色や表示バリエーションなど、意図の分かる単位で渡したい。隔離の強さだけでなく、後から変えられる範囲を残すための設計です。
今回のサイトでは、共有欄の見出しは記事側、操作部品はプラグイン側という分担で扱えます。どちらが読者へ何を伝えるかを決めておけば、部品の内部構造を無理にのぞき込んで装飾する必要が減ります。再利用しやすい部品とは、外から触ってよい場所が分かる部品でもあるんですね。別の場所で使いたくなったときにも、この分かりやすさが助けになります。
その変更は、なぜ隣の機能まで壊すのか
ここでdomain、application、infrastructure、presentationが出てきます。きれいに名前が並ぶと、それだけで整理できた気になってしまいますよね。ただ、フォルダーを分けても、処理どうしのつながりまで自動で変わるわけではありません。私が見たいのは「URL生成のテストにブラウザが必要か」「保存先を変えると描画コードまで変わるか」です。図の上できれいに分かれているかだけでなく、実際に差し替えたらどこまで直すことになるのか。ここまで見ると、分けた意味が分かってきます。
domainは共有先や操作の意味を表し、applicationは「要求を受けてどの処理を行うか」を組み立てます。infrastructureはClipboard APIなど外部の機能へ接続し、presentationは画面を描きます。ファイルを4つのフォルダーへ置けば完成ではありません。共有URLの判断を直すのにDOM操作まで変える必要がないか、ブラウザを起動せず確認できるルールは何か、という変更と検証の単位で境界を確かめます。
実際のコピー操作を一往復でたどると、境界がはっきりします。presentationが押されたボタンを特定し、applicationへ渡します。domainの判断でコピーを選び、applicationがコピーの契約を呼び、infrastructureがブラウザへ書き込みます。完了したら成功案内へ、拒否されたら手動コピーの案内へ進みます。画面に「コピーしました」と出るのは、入口を押した直後ではなく、書き込みが成功した後です。
この実行順と、ソースコードの依存の向きは分けて考えます。内側のapplicationは「コピーできる道具」の契約を使い、外側のinfrastructureがその契約を実装します。実行するときに外側を呼ぶからといって、内側がnavigator.clipboardの具体的な使い方まで知る必要はありません。料理の注文票が厨房の機種を指定しないのと同じです。
もう少し中へ進んでみましょう。大事な判断をブラウザの具体的なAPIへ直接つなぐと、そのAPIを変えたいときに判断のコードまで直すことになります。必要な能力を先に契約として決め、その外側へ実装をつなげば、テストでは代わりの実装を渡せます。これが依存性逆転の考え方です。ただ、何でも抽象化すると、読むためにあちこちのファイルを行き来することにもなります。私も、小さな処理まで一律に分けるのではなく、変更や検証に意味のある境界なのかを考えるようにしています。
レイヤーの話を、共有URLを作る場面へ戻してみましょう。「タイトルに含まれる&をどう扱うか」は、画面の色を決める仕事ではありません。「コピーする」ことを選んだとき、Clipboard APIをどう呼ぶかも、SNSごとのURLルールとは別です。変更の理由が違うものを離しておくと、ブラウザが使えないテスト環境でも、URLの判断だけを確かめられます。
Ports and Adaptersを使うなら、メソッドの形だけでなく、失敗の契約まで揃えたいところです。コピー処理なら、利用不可・権限拒否・成功を利用側へどう伝えるか。テスト用の実装だけいつも成功し、本番のアダプターだけ黙って失敗するなら、差し替え可能とは言えません。ポートは単にブラウザAPIを一対一で包むためのものではなく、アプリケーションが必要とする振る舞いを固定する場所、と捉えています。
Clipboard APIの代わりにテストダブルを差し込めば、渡したURLと呼び出し回数、拒否時の分岐をブラウザなしで確かめられます。ただし、テストダブルが実装側の思い込みをそのまま再現していたら、両方まとめて間違います。そこで契約の検証と、実ブラウザでの権限・ユーザー操作の確認を分けます。どちらかを増やせばもう片方が不要、とはならないところが面白いですね。速いテストには速いテストの担当があります。
フォルダーの名前より、依存の向きを見てみてください。ドメインのコードが画面のquerySelectorやWordPressの関数を直接呼んでいれば、置き場所がdomainでも環境へ強く結び付いています。逆に、すべてを別ファイルへ分ければよいわけでもありません。変更時に一緒に考えたいものは近くへ置き、環境に振り回されたくない判断を内側へ残す。私はその程度の具体的な問いから始めたいと思っています。
ただし、公開実装を理想化して説明しないことも大切です。今回のPromariSnsShareElementは、表示だけでなく、具体的なブラウザ実装を選んで接続する入口も担います。applicationのClickContextやNotifierPortには、通知位置を示すElementへの参照も残っています。したがって「四層に分けたから、すべてがブラウザと完全に無関係」とは言えません。今の境界で検証できることと、さらに独立させたい点を区別します。
将来、接続の組み立てが複雑になったら、その入口を別の場所へ集める案があります。ただし小さな部品で最初からファイルを増やすと、処理を追う往復も増えます。分割前後で、共有先追加時の差分、ブラウザなしで試せる判断、公開契約の読みやすさを比べます。設計の良し悪しをフォルダーの数で決めないための、具体的な比較です。
仕組みの定義は、HTML仕様のCustom Elements、whenDefined()、React公式の解説、Ports and Adaptersの原著を参照できます。
SOLIDは、小さな共有ボタンにも必要か
SOLIDは5つの設計原則をまとめた呼び名です。単一責任は「クラスを小さくする」ことだけではなく、変更する理由を揃えること。開放閉鎖は、新しい振る舞いを足すとき既存の判断をどれだけ変更せずに済ませられるか。リスコフの置換は、契約を満たす実装を差し替えても利用側の前提が崩れないこと。インターフェース分離は、利用しない能力まで依存させないこと。依存性逆転は、具体的な道具ではなく必要な契約へ依存させることです。
では、その「契約」をコードで見てみましょう。PHP側では、共有先にServiceInterfaceという約束を持たせました。名前、ラベル、共有URL、アイコン、ブランド色、操作の種類。この6つを答えられれば、描画する側はXだけを特別扱いせずに処理できます。以下は公開実装と同じシグネチャです。名前空間とuse宣言も載せていますので、どの型へ依存しているかも合わせて確認できます。
このインターフェースで固定したいのは、描画側がサービス固有のクラスへ踏み込まなくても必要な情報を取得できることです。実装の継承関係は知らなくてよい。一方で、戻り値の型だけ揃っても、URLを開く操作なのか、コピーなのかが分からなければ実行できません。そこで操作の種類も契約へ含めています。共通化するほど、差分を表す情報は明示する。差分を消すことと、扱える形にすることは違います。
この分離が役立つのは、変更が入ったときです。Xの共有URLの決まりが変わったとして、LINEの定義やコピー処理まで修正するなら、関係のない仕事が絡み合っています。逆に、サービスごとの定義を直して、共通の契約テストで確認できれば、変更の影響を追いやすくなります。原則の略語を覚えるより、変更の前後で触るファイルと、確かめる振る舞いを比べるほうが実感できます。
SRPは「一つのクラスには一つの変更理由を」、OCPは「拡張を加えるとき、安定した判断をなるべく触らずに」。ここでいう一つの理由は、行数の少なさではありません。名前、色、URLが同じ共有サービスの仕様として一緒に変わるなら、まとめて理解できる良さがあります。何でも細分化して、意味が十個のファイルへ散らばると、かえって読みにくくなります。
下のコードでは、shareUrl()とaction()の組み合わせに注目してください。URLを持つサービスだけを前提にすると、CopyやNativeを足したところで利用側の分岐が破綻します。型として呼べること、戻り値が契約どおりであること、その操作に意味があること。この三つを契約テストで確かめます。インターフェースを作って終わりにしないための確認です。
この先のコードで受け取るShareRequestは、共有したい内容をまとめた依頼票です。URLやタイトルをばらばらの引数として渡す代わりに、一つの値として扱います。また、ここで呼ぶaction()はWordPressのactionフックではなく、共有操作の種類を返すメソッドです。同じ綴りでも、どこで定義された名前かを確かめると読み違いを防げます。
<?php
declare(strict_types=1);
namespace PromariSnsShare\Contracts;
use PromariSnsShare\Domain\Action;
use PromariSnsShare\Domain\ShareRequest;
interface ServiceInterface
{
public function key(): string;
public function label(): string;
public function shareUrl(ShareRequest $request): string;
public function icon(): string;
public function brandColor(): string;
public function action(): Action;
}「クラスを足せば終わり」にしない拡張の契約
ここまで読むと、「では、クラスを一つ足せば完成?」と思われるかもしれません。実は、もう少し作業が残っています。ServiceRegistryへの登録、設定、生成物、テストも更新します。このあたりは、手で整える必要があるんですね。私が避けたいのは、追加のたびに既存サービスのURL生成や共通の描画まで直すことです。登録の一か所を変更するのと、安定している分岐を何か所も変更するのでは、確認する範囲が違います。OCPを説明するなら、この差まで含めたいですね。
また、CopyやNativeはSNSのWebページへ飛ぶ操作ではありません。共通の契約へ載せる場合、利用側がすべての実装を「開くURLを持つもの」と思い込んでいないかを確かめます。Actionによって実行方法を分け、空のURLや利用できないAPIも含めて振る舞いを検証します。原則の名前を満たすより、実装を入れ替えたとき利用者に何が起こるかを確認することが重要です。
LSPをこの実装で考えると、問題は「同じ型のオブジェクトを渡せるか」だけではありません。利用側がすべてを外部URLとして開くなら、URLを持たないCopyはその前提を満たせません。空文字を返してごまかすと、エラーが利用側へ押し出されます。Open・Copy・Nativeを操作の契約として扱い、実行方法を選ぶところへ差分を集める。置換したときに守るべき事後条件も、操作ごとに確認します。
ISPとDIPも、実際に依存をたどると話が早いです。URLを組み立てる処理に保存の能力まで要求していないか。アプリケーションが必要な操作を、ブラウザAPIの具体名で直接固定していないか。抽象化を増やすこと自体が目的ではありません。テストで代替できる、変更の波及を止められる、といった利点がある境界に絞ります。小さな処理のたびにファイルを行き来する構造は、私も避けたいところです。
五つの原則を別々の採点項目にするより、一つの変更で試すと理解しやすくなります。「新しい共有先を足す」とき、関係する定義だけを変更できるか、古い利用側が同じ契約で動くか、不要な保存機能を実装させられないか、ブラウザ抜きでURLを検証できるかを見ます。略語は、その問いへ戻るための見出しとして使います。
登録を集めるServiceRegistryには、どの実装を使うかを見渡せる良さがあります。自動探索なら登録漏れを減らせる場合もありますが、何がどの順番で読み込まれるかを追う仕組みも必要です。今回の登録処理を見て、「少し手作業があるからOCP違反」と即断する必要はありません。重要なのは、共有先を追加したとき既存サービスの判断まで毎回書き換える構造になっていないかです。
新しい共有先を追加できたところで、前からあるボタンも一緒に見てみましょう。新しいボタンが出るだけでは、前からあるボタンのURLや並びが壊れていないとは言えません。未知のキーを渡した場合も含めて、どこで拒否されるかを決めておくと、設定ミスが画面まで流れ込みにくくなります。
タイトルに「&」が入っただけで壊れる理由
URLへつなぐ前に、文字の意味を守る
ここで少し、実験にお付き合いください。記事タイトルを TypeScript & PHP #1 に変えてみます。タイトルに記号を入れただけですが、これでURLの作り方の違いが見えてくるんです。
そのまま共有URLの末尾へつなげると、&は次のパラメーター、#はフラグメントとして解釈され、届けたかったタイトルが途中で別の意味になります。日本語や絵文字でも、URLの組み立てを曖昧にできません。XServiceは渡す項目を決め、共通のAbstractService::buildがキーと値をrawurlencodeで符号化します。空文字のパラメーターは送りません。「このタイトルでも壊れない?」と入力を一つ変えるだけで、共通処理を切り出す理由が見えてきます。
この処理はURLの構文を守るためのものです。任意のURLを安全な共有先に変える機能ではなく、HTML出力時のエスケープとも別です。入力の許可、URLの組み立て、HTML属性への出力という各境界で、異なる問題に対応します。次のコードはXServiceのURL生成部分です。iconなどの契約メソッドは省略しているため、この抜粋だけでクラス全体が完成するわけではありません。
URLの組み立てで揃えておきたいのは、関数へ渡す値が未エンコードなのか、すでにエンコード済みなのかです。タイトルの&を%26へ変換する処理が二か所にあると、次は%2526になってしまいます。見た目では気付きにくいんですよね。入力は生の値、キーと値の符号化は共通のビルダーが一度だけ担当する。この契約を固定すると、サービスごとの定義へ余計な加工が散らばりません。
よくあるつまずきは、完成したURL全体をまとめて符号化することです。https:// や ? まで値の一部として変えてしまうと、URLとしての骨組みが崩れます。逆に、すでに符号化した値へもう一度同じ処理をすると、% がさらに符号化され、意図しない文字列になることもあります。どの時点では生の値で、どの時点からURLの一部なのかを、処理の境界で揃えます。
HTML属性へ出す段階では、今度は引用符や&がHTMLの構文として解釈されないようにします。URLの組み立てとHTMLのエスケープは順番も役割も違います。テストの期待値を見るときも、ブラウザが実際に開くURLと、HTMLソースに書かれた表現を混同しないようにしましょう。画面に & が見えたから、必ずURLが壊れているとは限りません。
試す値は、日本語だけでなく、空文字、半角スペース、&、#、絵文字、すでにクエリを持つ記事URLも用意します。普段のタイトル一つで成功した処理が、区切りを含むタイトルでも動くか。小さな入力の工夫が、共有先ごとの巨大な画面テストより早く不具合を見つけてくれることがあります。
public function shareUrl(ShareRequest $request): string
{
return $this->build('https://twitter.com/intent/tweet', [
'url' => $request->url,
'text' => $request->text,
'hashtags' => $request->hashtagsCsv(),
'via' => $request->via,
]);
}変更できない・選べない形を、PHPの型で作る
「このクラスの一部分だけ、後から変えられる?」。そうできたら便利そうですよね。ただ、後から読むときには、どこで何が変えられたのかも追わなくてはいけません。XServiceをfinalにすると、そのクラスを継承して一部分だけ置き換えることはできません。別サービスを増やすなら同じ契約を実装し、どの実装を使うか登録側で明示します。これはすべての継承が悪いという意味ではなく、具体的なサービスの振る舞いを追いやすくする選択です。共通のURL構築はAbstractServiceへ集約しています。
ShareRequestはURL・タイトル・本文・ハッシュタグなどを持つ値オブジェクトです。readonlyプロパティにすると初期化後の再代入を防げます。サービスごとにURLを変えるwithUrlは、元の要求を変更せず新しい値を返します。ただしreadonlyは、参照先オブジェクトの内部まで自動的に不変にする仕組みではありません。何を保持するかと合わせて考えます。
ActionはOpen・Copy・Nativeの文字列-backed enumです。任意の文字列を渡すより、取り得る選択肢を限定できます。未知の文字列から変換する境界では、fromの例外やtryFromのnullを適切に扱う必要があります。PHPは実行時にも型を検査する言語なので、「綴りを間違えたら必ずコンパイル時に止まる」という説明は正確ではありません。型宣言と静的解析とテストが、それぞれ異なる時点で誤りを見つけます。
考えてみる:readonlyなら、受け取った値は正しい?
書き換えられないことと、正しいことは別です。形式の違うURLでも、検証せずに不変の値へ入れれば、そのまま保持されます。外から受け取る時点で確かめ、内部では不変の値として扱う。型と検証が受け持つ仕事を分けると、「型を付けたから安心」の先へ進めます。
値オブジェクトという名前は大げさですが、入口はシンプルです。「この要求を表す値のまとまり」を作り、どの項目が一緒に動くのかを明確にします。URLとタイトルを毎回ばらばらの順番で渡すより、ShareRequestとして受け渡せば、その処理が何を扱っているか読み取りやすくなります。配列のキーを一文字間違える問題も、型や生成時の検査へ寄せられます。
ここでreadonlyが効く場面を考えてみましょう。同じ要求からX向け、LINE向けと順番にURLを作る途中で、最初のサービスが要求のURLを書き換えたら、次のサービスは書き換え後の値を受け取ってしまいます。元の値を保ち、変更版は別の値として返せば、実行順序に引きずられにくくなります。変更しない約束は、処理の前後を覚えておく負担を減らすためにも使えます。
enumも、単に入力を短くする機能ではありません。「この操作は三種類のどれか」とコードへ書くと、第四の未知の操作が来たときに、どこで止めるかを考えられます。外部からの文字列をそのまま信用せず、変換できなかった場合を入口で扱う。内部へ入った後は限定された選択肢として処理する。この順番が、後で紹介するTypeScriptのunionや実行時検証にもつながります。
final・readonly・enumを全部付ければよい、という話ではありません。継承して差し替えてほしい場所にfinalを付ければ、必要な拡張まで止めます。変化する状態を無理に一つの不変値へ押し込めれば、扱いづらくなる場合もあります。どの自由を残し、どの変更を禁止したいか。その意図を言葉にしてから、言語の機能を選びたいですね。
型の機能を試すなら、正常な例を一つ書いた後、わざと違う種類の値を渡してみてください。開発中の型検査で止まるのか、実行したときに例外になるのか、それとも何も起きずに不正な値が残るのか。発見できる時点の違いが見えてきます。型・静的解析・入力検証・テストは競争相手ではありません。同じ間違いを見ているようで、守っている場所が違います。どの道具が、いつ気付かせてくれるかを知っておくと、安心できる範囲も正確になります。
PHPのreadonlyの範囲は、PHP公式のプロパティ解説を読むと、オブジェクトの内部状態との違いも確認できます。
型があるのに、なぜ実行時にも確かめるのか
PHPの型を見たところで、次はTypeScriptへ進みましょう。PHP側で契約を揃えても、ブラウザへ届くJSONまで型が保証してくれるわけではありません。コードを書くときに確かめられることと、実際にデータが届いてから確かめること。ここには違いがあるんですね。as LikeStateを書いた瞬間に検証を済ませた気になってしまうのが、いちばん厄介です。
型を付けておけば、書いている段階で渡し間違いに気付きやすくなります。共有先のキーや操作を文字列のunionで表すと、扱い忘れた場合も見つけやすくなりますよ。ただし、型の情報はビルド後のJavaScriptでは基本的に消えます。HTML属性やJSON、CustomEventのdetailが予定どおりの形かどうかは、受け取る場所で別に確かめなければなりません。
たとえばAPIがcountを返すと決めても、障害時にHTMLのエラーページやnullが届く可能性があります。数値として妥当か、負になっていないか、likedが真偽値かを確かめてから画面の状態にします。型アサーションで「正しい型だ」と宣言しても入力は検証されません。内部では扱いやすい型に揃え、境界では失敗を明示する。この二段構えが、型の強さを実際の利用者体験へつなげます。
型アサーションは変換でも検証でもありません。response.json()の結果をそのまま既知の型へ押し込めると、実際にはエラーページや別形式のJSONが来ていても、型検査の上では正常な状態に見えます。境界ではunknownとして受け、オブジェクトか、必須項目があるか、値域が正しいかを確認してから内部の型へ渡す。この順番なら、UIのあちこちへ防御的なチェックを散らさずに済みます。
たとえば { count: "12", liked: false } は、人間には12件と読めますが、countは数値ではなく文字列です。暗黙に足し算すると、1を足したつもりが文字列の連結になることもあります。仕様として文字列を許すなら明示的に変換し、数値だけを受けるなら拒否する。どちらを選んだかが分かることが大切です。as LikeState と書いても、この変換や検査が勝手に追加されるわけではありません。
unknownは「まだ何か分からない値」として受け取るための型です。何でも操作できるanyで境界を通してしまうより、型や項目の存在を確かめてから扱う流れを作れます。unionは複数の候補をまとめる表現で、たとえば成功と失敗を別の形に分けられます。成功時だけ件数を持つ形なら、失敗した応答からうっかり件数を読む誤りにも気付きやすくなります。
また、count がnumberであることだけでは、件数として妥当とは限りません。たとえば「いいねがマイナス1件」と表示されたら、何が起きたのかと思いますよね。小数や無限大を件数として受け取ってよいかも、考える必要があります。画面へ届くデータでは「型」と「業務上の意味」の両方を見ます。型が得意なところへ任せ、意味の検査は小さな関数へまとめる。こうすると、テストも「この変な値を受け取ったらどうする?」という形で書きやすくなります。
このページ(2ページ目)の感想・質問・設計へのコメント