TypeScript実務のコツ12選: anyを避けてstrictで事故らない型の書き方
型推論を活かす、anyをunknownと型ガードで置き換える、strict設定、as constとリテラル型、エラーの読み方。実務で効くTypeScriptのコツを一人称で。
error TS2345: Argument of type 'unknown' is not assignable...
金曜の夜、本番のフォーム送信が静かに壊れていました。原因はAPIレスポンスを any で受けていた1行。response.data.user.name が undefined でも、コンパイルは何も言わずに通っていたんです。
僕はその夜、any を一つ消すたびに事故が減ることを身体で覚えました。
TypeScriptの「コツ」って、難しい型パズルのことだと思われがちです。でも実務で効くのは地味なほうで、型推論に任せる、any を使わない、strict を入れる、リテラルで値を狭める、エラーを落ち着いて読む——この5つで、バグの大半は手前で止まります。今日はそこを、僕がやらかした失敗込みでまとめます。
ジェネリクスやユーティリティ型といった「部品の作り方」は別記事に切り出してあるので、この記事は横断的なコツに徹します。
この記事の要点
- 型は全部手書きしない。型推論に任せて、境界だけ手で書くのが速くて壊れにくい。
anyは型チェックを丸ごと無効化する。外から来る値はunknownで受けて、型ガードで絞ってから使う。tsconfigにstrictを入れるのが最大のコツ。noUncheckedIndexedAccessまで足すと配列・オブジェクトの抜けを拾える。as constとリテラル型で、stringを「許される値だけ」に狭める。asでの型キャストは最後の手段。- エラーは長くても怖くない。「実際の型 → 期待された型」の差分だけ読めば原因にたどり着く。
コツ1〜3: 型推論に任せて、境界だけ手で書く
最初に覚えてほしいのは、型を書きすぎないことです。TypeScriptは右辺から型を推論します。だから変数やローカルの戻り値にいちいち注釈を付けると、冗長なうえに、リファクタのたびに二重メンテになります。
公式の言い方だと「型注釈は必要なときだけ」。詳しくは TypeScript公式の Everyday Types が一次情報です。
// Before: 推論で十分なのに全部手書き。冗長で、型がズレるとメンテ漏れする
const count: number = 0;
const names: string[] = ["a", "b"];
function double(n: number): number {
return n * 2;
}
// After: 推論に任せる。引数の型だけ書けば、戻り値は勝手に number になる
const count = 0;
const names = ["a", "b"];
function double(n: number) {
return n * 2;
}
では、どこに手で型を書くのか。答えは境界です。関数の引数、公開する関数の戻り値、外部から来るデータ。ここだけは推論に任せず、自分で「契約」を宣言します。中身の計算過程はTypeScriptに推論させる。これがコツ1から3、つまり「推論に任せる・引数は書く・公開APIの戻り値は書く」のセットです。
なぜ公開関数の戻り値は書くのか。書いておくと、実装をうっかり変えて戻り値の形が変わったとき、呼び出し側ではなくその関数自身でエラーが出るからです。事故の発生源で止まる。デバッグが一気に楽になります。
// 公開する関数は戻り値を明示。実装が契約から外れたら、この行でエラーになる
export function getTax(price: number): { net: number; tax: number } {
const tax = Math.round(price * 0.1);
return { net: price, tax }; // ここで形が崩れると即エラー
}
コツ4〜6: anyを捨てて、unknownと型ガードで受ける
ここが本丸です。any は「型チェックを止めてくれ」という指示だと思ってください。any を一つ置くと、そこから先のプロパティアクセスもメソッド呼び出しも、全部ノーチェックで通ります。冒頭の金曜の事故は、まさにこれでした。
代わりに使うのが unknown です。unknown は「まだ何かわからない値」。any と違って、そのままでは何もできません。プロパティを読むにも、まず「これは○○型だ」と絞る必要がある。この「絞る」処理が型ガードです。
| 受け方 | プロパティを直接読める? | 安全性 | 使う場面 |
|---|---|---|---|
any | 読める(ノーチェック) | 低い。事故の温床 | 原則使わない |
unknown | 読めない(要・絞り込み) | 高い | 外部入力の入口 |
| 具体的な型 | 読める(チェック済み) | 高い | 絞り込んだ後 |
外から来る値——fetch のレスポンス、JSON.parse の結果、localStorage、フォーム入力——は、実行時には何が入っているかわかりません。だから unknown で受けて、型ガードで形を確認してから使います。これがコツ4「any の代わりに unknown」、コツ5「型ガードで絞る」です。
型を絞る基本テクニックは TypeScript公式の Narrowing にまとまっています。typeof、in 演算子、そして自分で書く「型ガード関数」です。
// 型ガード関数: 戻り値の「value is User」が"これはUserだ"とTypeScriptに教える目印
type User = { id: string; name: string };
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as Record<string, unknown>).id === "string" &&
typeof (value as Record<string, unknown>).name === "string"
);
}
// 使う側: unknownで受けて、ガードを通った中だけ User として扱える
function greet(input: unknown): string {
if (!isUser(input)) {
throw new Error("User型ではありません");
}
return `こんにちは、${input.name} さん`; // ここでは input は User 型
}
コツ6は「境界の数を増やさない」。型ガードを毎回手書きするのは正直しんどいので、入口を絞るのがコツです。フォーム、API、Webhookのように「壊れた値が入りそうな場所」を数か所に集約して、そこだけ丁寧にガードする。中で何度も unknown を受け直さない。検証ライブラリを使うなら、Zodで入力バリデーションを型安全にする手順 にスキーマから型を導出するやり方をまとめています。手書きのガードより楽で、実行時チェックと型が一致します。
コツ7: tsconfigにstrictを入れる(これが一番効く)
正直、この記事で一個だけ持ち帰るならこれです。tsconfig.json の strict: true。
strict は複数のチェックをまとめてオンにするスイッチです。中でも効くのが strictNullChecks——null や undefined を勝手に無視させない設定。冒頭の user.name が undefined だった事故は、strict が入っていれば「user は undefined かもしれません」とコンパイル時に止まっていました。
公式の説明は TSConfigのstrict にあります。実務で僕が足しているのはこのあたりです。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
noUncheckedIndexedAccess は、配列やオブジェクトからキーで値を取るときに undefined の可能性を残す設定です。const first = list[0] の first が T | undefined になる。最初は面倒に感じますが、空配列・設定ファイルのキー抜け・APIの欠損フィールドを、実行前に拾えます。
// noUncheckedIndexedAccess あり: list[0] は string | undefined
const list = ["a", "b"];
const first = list[0];
// console.log(first.toUpperCase()); // エラー: first は undefined かもしれない
console.log(first?.toUpperCase()); // OK: 存在チェックを強制される
既存プロジェクトでいきなり全部オンにするとエラーが噴き出します。そのときは一気にやらず、まず strict だけ入れて、出たエラーを潰してから一個ずつ足すと事故りません。
コツ8〜9: as constとリテラル型で値を狭める
string という型は広すぎます。例えば料金プランを plan: string と書くと、"freee"(タイプミス)も "PRO"(大文字違い)も通ってしまう。これを「許される値だけ」に狭めるのがリテラル型です。
// Before: string は何でも入る。タイプミスがコンパイルを通り抜ける
type PlanLoose = { name: string };
const a: PlanLoose = { name: "freee" }; // 通ってしまう…
// After: リテラルのUnionで、3つの値だけに限定する
type Plan = "free" | "pro" | "enterprise";
type PlanStrict = { name: Plan };
// const b: PlanStrict = { name: "freee" }; // エラー: タイプミスを即検出
const b: PlanStrict = { name: "free" };
コツ9は as const です。オブジェクトや配列に付けると、中身を「書き換え不可・リテラルそのまま」で固定してくれます。設定表やルート定義に効きます。
// as const なし: method は string に広がる
const routeLoose = { method: "GET", path: "/users" };
// routeLoose.method の型は string
// as const あり: method は "GET" というリテラルのまま固定される
const route = { method: "GET", path: "/users" } as const;
// route.method の型は "GET"
// 配列から値のUnion型を作る定番テク
const PLANS = ["free", "pro", "enterprise"] as const;
type PlanFromArray = (typeof PLANS)[number]; // "free" | "pro" | "enterprise"
PLANS を as const で固定し、(typeof PLANS)[number] で型に変換する。こうすると値と型が1か所で同期します。プランを追加するときは配列に足すだけ。型が自動で広がります。なお as const(const アサーション)と、型に合うかだけ検査して値はそのまま残す satisfies は別物です。satisfies を含む「型の部品化」は ジェネリクス入門記事 と ユーティリティ型入門記事 に分けて書いています。
コツ10: asでの型キャストは最後の手段
as は便利ですが、型エラーを”消す”だけで、実際の値は何も変えません。response as User と書いても、中身が User でなければ実行時に普通に壊れます。コンパイラに嘘をついているだけ。
// アンチパターン: as で黙らせる。中身が違っても実行時に壊れる
const data = JSON.parse(text) as User;
console.log(data.name); // text が壊れていても、ここまで素通り
// 推奨: 型ガードで本当に User か確かめてから使う
const parsed: unknown = JSON.parse(text);
if (isUser(parsed)) {
console.log(parsed.name); // 検証済みなので安全
}
僕は昔、as で型エラーを片っ端から消して「ビルド通った!」と喜んでいました。結果、型システムが何も守ってくれない、any だらけと変わらないコードになった。as を書きたくなったら、それは「型ガードを書くべき場所」のサインだと思っています。例外は、自分が値の形を100%わかっていてTypeScriptに伝えきれないとき(テストのモックなど)だけです。
コツ11: エラーメッセージは差分だけ読む
TypeScriptのエラーは長くて、初心者ほど見た瞬間に閉じたくなります。でも構造はだいたい同じで、読むべきは**「実際の型」と「期待された型」の差分**だけです。
代表的なエラーの読み方を表にします。
| エラー番号 | 雑な意味 | だいたいの原因 |
|---|---|---|
| TS2345 | 渡した引数の型が合わない | unknown を絞らず渡した/型違い |
| TS2322 | 代入先の型に合わない | 戻り値や変数の型がズレた |
| TS2531 / TS18047 | null/undefined かもしれない | strictNullChecks の存在チェック漏れ |
| TS7006 | 引数が暗黙の any | 引数に型注釈がない |
| TS2339 | そのプロパティは存在しない | 型が広すぎる/絞り込み前 |
例えばこんなエラー。
Argument of type 'string | undefined' is not assignable to
parameter of type 'string'.
Type 'undefined' is not assignable to type 'string'.
長く見えますが、要するに「undefined の可能性がある値を、undefined を許さない場所に渡した」だけです。?. か存在チェックを足せば直る。エラーの最後の1〜2行に本当の原因が書いてあることが多いので、僕はいつもそこから読みます。
慣れるまでのコツは、エラーを2つに分解することです。1つ目は「TypeScriptが何だと思っているか(実際の型)」、2つ目は「TypeScriptが何を期待しているか(期待された型)」。この2つの差分が、そのまま直すべき箇所になります。型が複雑で読みにくいときは、その変数にいったん明示の型注釈を付けてみると、どこでズレているかが切り分けやすくなります。AIにエラーを貼って直してもらうときも、「このエラーの原因と、最小の修正だけ教えて」と頼むと、関係ない箇所まで書き換えられずに済みます。
コツ12: AIに書かせる前に型ルールを渡す
Claude CodeのようなAIにTypeScriptを書かせると初速は出ますが、放っておくと any と広すぎる string を量産します。AIは「動くこと」を優先するからです。だから依頼の前に、短い型ルールを渡しておくのがコツです。
このリポジトリは strict TypeScript 前提です。
- any は使わず、外部入力は unknown で受けて型ガードで絞ってください
- string で済ませず、決まった値はリテラルUnionにしてください
- as での型キャストは避け、型ガードか satisfies を使ってください
- 実装後に npx tsc --noEmit を通してから報告してください
このルールを CLAUDE.md に置いておくと、毎回貼らなくても効きます。長いプロンプト集より、こういう短い「型の境界線」のほうが長く効きます。
コピペで動く最小サンプル
ここまでのコツ(推論・unknown・型ガード・リテラル型・as const)を1ファイルに詰めました。npx tsc --noEmit が通り、npx tsx このファイル で動きます。
// plan-check.ts
// プランを表す値を1か所で定義(値)→ そこから型を導出(as const)
const PLANS = ["free", "pro", "enterprise"] as const;
type Plan = (typeof PLANS)[number]; // "free" | "pro" | "enterprise"
type Account = { id: string; plan: Plan };
// 外から来た unknown を Account に絞り込む型ガード
function isAccount(value: unknown): value is Account {
if (typeof value !== "object" || value === null) return false;
const v = value as Record<string, unknown>;
return (
typeof v.id === "string" &&
typeof v.plan === "string" &&
(PLANS as readonly string[]).includes(v.plan)
);
}
// any を使わず unknown で受ける。戻り値の型は推論に任せてOK
function describe(input: unknown) {
if (!isAccount(input)) {
return "不正なアカウントデータです";
}
// ここでは input は Account 型として安全に使える
const label: Record<Plan, string> = {
free: "無料プラン",
pro: "Proプラン",
enterprise: "Enterpriseプラン",
};
return `${input.id}: ${label[input.plan]}`;
}
// 実行時に壊れた値が来ても、ガードが弾く
const raw: unknown = JSON.parse('{"id":"a1","plan":"pro"}');
console.log(describe(raw)); // a1: Proプラン
console.log(describe({ id: "a2", plan: "PRO" })); // 不正なアカウントデータです
ポイントは、PLANS という値から Plan という型を1行で作っている点です。プランを増やすときは配列に足すだけで、型・ガード・ラベルの網羅チェックが全部追従します。label を Record<Plan, string> にしてあるので、プランを足してラベルを書き忘れると、ここでエラーになります。
よくある質問
Q. any と unknown、結局どっちを使えばいい?
外部から来る値は必ず unknown。any は「型チェックを切る」指示なので、原則ゼロを目指します。unknown で受けて型ガードで絞れば、安全なまま具体的な型として使えます。
Q. strict を既存プロジェクトに入れたらエラーが数百件出ました。
普通です。一気に直さず、まず strict だけ入れて、ファイル単位かエラー種別ごとに潰していきます。noUncheckedIndexedAccess のような追加設定は、strict が落ち着いてから一個ずつ足すと混乱しません。
Q. as は絶対に使ってはいけない?
禁止ではなく「最後の手段」です。値の形を自分が100%わかっていて、かつTypeScriptに伝えきれないとき(テストのモックなど)に限ります。外部入力を as で黙らせるのは、any と同じくらい危険です。
Q. リテラル型と enum、どっちがいい?
多くの実務では "free" | "pro" のようなリテラルUnion + as const で足ります。追加の実行時コードを生まず、JSONとも相性がいいからです。enum は名前空間が欲しい特定の場面で検討すればOK。
Q. 型注釈はどこまで書くべき? 引数と、公開する関数の戻り値だけ手で書いて、あとは推論に任せるのが基本です。ローカル変数まで全部書くと冗長で、リファクタのたびにメンテ漏れが起きます。
実際に試した結果
冒頭の金曜の事故のあと、僕がやったのは派手なリファクタではありませんでした。tsconfig に strict を入れ、APIの入口を unknown + 型ガードに変え、plan を string からリテラルUnionに狭めた。それだけです。
効果は地味だけど確実でした。次の週、Claude Codeに別のフォームを書かせたとき、undefined 参照とタイプミスのプランがコンパイル時に止まりました。本番で気づいていたら、また金曜の夜が消えていたやつです。
TypeScriptのコツは、難しい型を書けるようになることじゃない。「ここから先は推測で進むな」とAIにもコンパイラにも伝える境界線を、数か所だけ丁寧に引くことだと、今は思っています。まずは strict を1行入れるところから。
型ルールを個人で整えるチェックリストやテンプレートは 教材一覧 に置いています。チームの既存リポジトリをまとめてstrict化したいなら 研修・相談 からどうぞ。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。