この記事の目次
設定表に型を付けると誤字は見つかったのに、文字列だと分かっている項目へtoUpperCaseを呼べなくなった。型注釈で広い契約にそろえることと、具体的な値の型を使うことが、同じ結果にならない場合があります。
JavaScriptのオブジェクト、TypeScriptの型注釈とユニオンが分かる人向けです。架空の貸出端末の表示設定を使い、数値か文字列を受け付ける二つのキーを検査します。APIから来たJSONの検証ではなく、ソースコードに直接書いた設定が対象です。
「許される値」と「今回書いた値」を分ける
この設定ではcompactとspaciousを必須のキーにし、値は数値または文字列にします。例ではcompactへ数値12、spaciousへ文字列「ゆったり」を入れます。数値は表示間隔、文字列はプリセットの呼び名を模した練習用の契約です。
Record<Mode, number | string>は、Modeの各キーを持ち、それぞれに数値か文字列を要求する型です。Recordの公式説明にあるキーと値の対応を利用します。値をCSSへ反映する処理や、プリセットから数値へ変換する処理は含みません。
型注釈で変数全体をこの型にすると、compactを読んだときもnumberかstringのどちらかとして扱います。一方、satisfiesは契約への適合を検査しながら、この例ではcompactのnumber、spaciousのstringという具体的な情報を使えます。TypeScript 4.9の公式資料で導入された演算子ですが、ここでは5.9.3へ固定して比較します。
「satisfiesなら推論が一切変わらない」とは覚えないでください。対象の型が推論へ文脈を与える場合もあります。実際に得られる型を確かめるのが目的で、数値リテラル12そのものまで常に保持するという保証ではありません。
同じ設定を二通りに書き、呼べるメソッドを比べる
Node.js 24系とnpmを用意し、空のフォルダでnpm init -y、npm install --save-dev --save-exact typescript@5.9.3を実行します。次をconfig.tsへ保存してください。最初は両方の定義を残し、satisfies側だけを出力します。
type Mode = 'compact' | 'spacious';
type Settings = Record<Mode, number | string>;
const annotated: Settings = {
compact: 12,
spacious: 'ゆったり'
};
const checked = {
compact: 12,
spacious: 'ゆったり'
} satisfies Settings;
console.log(checked.compact.toFixed(1));
console.log(checked.spacious.toUpperCase());
次で厳密な型検査と変換を行い、成功後にJavaScriptを実行します。package.jsonのtypeは変更しない構成です。プロジェクト共通のtsconfigは使わず、比較に必要な条件をコマンドへ明記します。
npx tsc config.ts --strict --target ES2022 --module commonjs --noEmitOnError --outDir out
node out/config.js
出力は「12.0」「ゆったり」の二行です。日本語の文字はtoUpperCaseでも変わりません。メソッドが呼べることを見たいので、文字が変わる例も試すならspaciousを「wide」に変更し、二行目が「WIDE」になるか確認します。
次に、一時的にconsole.log(annotated.compact.toFixed(1));を末尾へ追加して型検査します。stringの可能性があるため、toFixedが存在しないという診断になります。これは実行させる完成コードではなく、型注釈との差を観察する一行です。記録後は取り除いてください。
annotated側でもtypeofでnumberへ絞れば呼べます。どちらが常によいかではなく、ここで広い型として扱いたいのか、書いた設定の具体的な型を使いたいのかが選択点になります。
足りないキーと、余分なキーを別々に壊す
第一の失敗はcheckedからspaciousの行を削ることです。satisfies Settingsの位置で必須のspaciousがないと診断されます。修正はキーを追加し直すことです。optionalに変えてエラーを消すと、二種類を必ず定義する契約そのものが変わります。元へ戻して型検査と二行の出力を再確認します。
第二の失敗は、spaciousをspacousへ誤記することです。直接書いたオブジェクトでは未知のキーとして拒否されます。キーの綴りを直し、compactとspaciousの二つがそろうことを確認します。Record<string, ...>へ広げると名前の候補を限定できなくなるため、誤字の修正にはなりません。
さらにcompactをtrueへ変更すると、キーが正しくても値の型が合わず拒否されます。修正ではnumberまたはstringへ戻します。キーの一覧と各値の形式は別の検査なので、片方だけ壊した例を順番に試すと、診断の理由を追いやすくなります。
いずれも型検査が失敗した後に、以前生成したout/config.jsを実行して成功と数えないでください。noEmitOnErrorは誤った新しい出力を作らない指定であり、過去の出力ファイルを消す指定ではありません。まず検査の終了結果を確認し、成功した回の出力だけを評価します。
satisfiesを、読み取り専用や外部検証へ広げない
checked.compactはこの例ではnumberなので、後から18を代入できます。satisfiesはオブジェクトを凍結する操作ではありません。readonlyや実行時の変更防止が必要なら、その目的に合う別の設計を選びます。as constを併用すると型の狭さとreadonlyの扱いも変わるため、今回の比較へ無条件に付け足しません。
また、別の変数を経由したオブジェクトの余分なキーが、常に拒否されるわけではありません。TypeScriptの構造的な互換性と、直接書いたオブジェクトに対する余分なプロパティの検査は区別します。「satisfiesを付ければ余分な情報をすべて削除できる」という機能ではなく、実行時には値を加工しません。
外部から届いたデータなら、unknownで受信データを検査する記事のように、値を実際に読む処理が必要です。この設定をJSON.parseの結果やanyへ替えても、satisfiesだけでは実行時の誤りを防げません。
一方、後からcompactへ文字列も代入する共通の設定ストアなら、最初からSettingsという型注釈で広い契約を保つ選択があります。個別の型を狭く推論させることと、将来許す更新の範囲が合っているかを考えます。
三種類の診断と、正常な出力を記録する
15分程度の練習として、完成例、不足キー、誤記キー、boolean値を一つずつ試します。完成例だけが型検査を通り二行を出す、不足は必須項目の診断、誤記は未知のキー、trueは値の型の診断になるのが判定基準です。最後は完成例へ戻して終了します。
追加の問いとして、checked.compactへ18を代入する一行を出力前へ入れてみます。型検査は通り、先頭が「18.0」になるのが解答です。同じ位置へ文字列を代入すると、推論されたnumberには合いません。この差で、契約のnumber | stringと今回の値の型を分けて説明できます。
JavaScriptの無料レッスンで設定オブジェクトを扱うときにも、まず必要なキーと値を書き出せます。型検査で何を拒否できたかと、実行で何が出たかを別の欄に残すと、satisfiesへ期待してよい範囲が明確になります。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。