なぜ、あなたの書くコードは `as` だらけになるのか?
コードレビューをしていて、一番げんなりするのはどこだか分かるか?
膨大な `as unknown ashh…` や、あまつさえロジックを殺す `as any` の乱用だ。
「型が合わないから、とりあえずアサーションで黙らせた」
——そんな言い訳は、俺の前では通用しない。
TypeScriptのコンパイラ(tsc)は、お前の敵じゃない。世界で一番優秀な相棒だ。だが、お前が「コンパイラが推論しやすい型パズル」の組み方を知らないから、相棒の目を塞ぎ、手足を縛っている。
今回は、インターフェースと型エイリアス(Type Alias)を駆使し、コンパイラの型推論能力を限界まで引き出し、`as` をコードベースから根絶やしにするための極限の知見を授けよう。
—
1. 「広すぎる型」が推論を殺す
まず、多くのジュニア〜ミドルクラスのエンジニアが犯す最大の過ちを見てみよう。非同期APIからステータスを取得するだけの、一見無害なコードだ。
// ❌ 最悪な例:プリミティブの型が広すぎて推論が機能しない
type FetchState = {
status: string; // “loading” | “success” | “error” にしたいが string にしている
data: unknown;
error: Error | null;
};
function handleState(state: FetchState) {
if (state.status === “success”) {
// コンパイラは state.status が “success” だと絞り込めるが、
// data が unknown のままなので、結局ここで as を使う羽目になる
const user = state.data as User;
console.log(user.name);
}
}
なぜこうなるのか? `status` を `string` 型で定義した瞬間、TypeScriptは「この変数は書き換え可能であり、あらゆる文字列が入り得る」と判断する。そのため、`if (state.status === “success”)` と比較しても、コンパイラは `status` を `”success”` というリテラル型に狭める(Narrowing)ことができず、結果として `data` の型連動も完全に破壊される。
解決策:厳密なリテラル型と「判別可能なunion(Discriminated Unions)」
コンパイラに「因果関係」を教え込むには、union型とリテラル型を組み合わせる。
// ⭕ 完璧な例:状態の排他性をコンパイラに完全に理解させる
type FetchState
| { status: “idle”; data: null; error: null }
| { status: “loading”; data: null; error: null }
| { status: “success”; data: T; error: null }
| { status: “error”; data: null; error: Error };
// 使用例
function handleState
// コンパイラはこの分岐を見た瞬間、他の可能性を完全に排除する
if (state.status === “success”) {
// ここでの state は { status: “success”; data: T; error: null } に確定している!
// したがって、data はキャストなしで T 型として安全に扱える。
console.log(state.data);
}
}
この設計であれば、`as` を使う必要は1ミリもない。コンパイラは自律的に型を絞り込み、バグの入り込む隙間を消し去る。
—
2. オブジェクトの「構造の硬直化」を防ぐジェネリクス型エイリアス
フロントエンド開発で最も頻発するのが、「汎用的なコンポーネントプロパティやAPIクライアントの型定義」だ。ここで、オプショナルや `undefined` の付け方を誤ると、推論は一気に崩壊する。
次のコードを見てほしい。APIのペイロードを構築する関数だ。
// ❌ 悪い例:オプショナルとundefinedの混同、そして狭められない型
type RequestOptions = {
method?: “GET” | “POST”;
headers?: Record
body?: unknown;
};
// これを使う側で無駄なasが必要になる
const opt: RequestOptions = {
method: “POST”,
body: { userId: 1 } // ここが unknown になり、型安全性が死ぬ
};
解決策:`const` アサーションと `satisfies`、そして条件付き型の活用
TypeScript 4.9以降、`satisfies` 演算子が導入された。これにより、「型制約を満たしつつ、推論されたリテラル型を保持する」という離れ業が可能になる。
さらに、関数引数において「渡されたオブジェクトの形状を正確に推論させたい」場合は、ジェネリクスを噛ませるのが定石だ。
// ⭕ 高度な型エイリアスによるAPIクライアント設計
type HttpMethod = “GET” | “POST” | “PUT” | “DELETE”;
type ApiRequestConfig
method: TMethod;
url: string;
// POSTやPUTの時だけbodyを必須にする条件付き型エイリアス
body: TMethod extends “POST” | “PUT” ? TBody : never;
headers?: Record
};
// 型推論を最大限に活かすヘルパー関数
function createRequest
config: ApiRequestConfig
): ApiRequestConfig
return config;
}
// — 実際の呼び出し —
// コンパイラは method が “POST” であることを検知し、
// body プロパティが必須であり、かつ渡したオブジェクトの型を正確に保持することを推論する
const req = createRequest({
method: “POST”,
url: “/api/user”,
body: { name: “Architizer”, age: 30 }, // TBody が { name: string; age: number; } と推論される
});
// req.body はちゃんと { name: string; age: number; } として扱われる。as は不要。
もしこれが `method: “GET”` であれば、コンパイラは `body` プロパティが存在すること自体をコンパイルエラーとして弾き返す。これが、型推論を最大化したモダンなTypeScriptの姿だ。
—
3. `as const` の魔術:設定値やUIステートの定数化
フロントエンドのUIコンポーネントで、タブやバリアントの定義を配列やオブジェクトで持たせることは多いだろう。ここで `as const` を使いこなせているかで、そのエンジニアの練度が測れる。
// ❌ よくある凡庸なコード
const BUTTON_VARIANTS = [“primary”, “secondary”, “danger”];
// これだと string[] になり、型が広すぎる
type ButtonVariant = typeof BUTTON_VARIANTS[number]; // “primary” | “secondary” | “danger” になるが…
これだと、配列への後からのプッシュや変更に対する耐性がない。厳格にオブジェクトとして定数化し、型を抽出するのがプロダクションクオリティだ。
// ⭕ プロダクション品質の定数定義と型エイリアス
export const THEME_CONFIG = {
colors: {
primary: “#007acc”,
secondary: “#6c757d”,
danger: “#dc3545”,
},
spacing: {
sm: “4px”,
md: “8px”,
lg: “16px”,
},
} as const; // 全ての値と構造を readonly のリテラルとして確定させる
// 型エイリアスを自動生成する(keyof の合わせ技)
export type ThemeColors = keyof typeof THEME_CONFIG.colors;
export type ThemeSpacing = keyof typeof THEME_CONFIG.spacing;
// 使用例:コンポーネントのProps
type ButtonProps = {
variant: ThemeColors;
padding: ThemeSpacing;
};
// 型推論が完全に働き、IDEの補完が爆速で効く
const myButtonProps: ButtonProps = {
variant: “primary”, // “primary” | “secondary” | “danger” 以外を入れると即コンパイルエラー
padding: “md”,
};
—
チーフアーキテクトからの総括
型アサーション (`as`) は、TypeScriptという偉大なコンパイラに対する「敗北宣言」だ。
お前たちが書くべきは、アサーションで無理やり型をねじ曲げたコードではない。コンパイラが自ら「あ、ここは絶対にこの型しかあり得ないな」と自動的に推論できる、美しく論理的な型パズルなのだ。
今日からコードを書くときは、`as` を打つ前に手を止めろ。
「どうすればコンパイラにこの文脈を理解させられるか?」を考え、InterfaceとType Aliasの構造を見直すんだ。
その一手間が、お前のコードベースを永遠に保守しやすく、バグ知らずの要塞へと変貌させる。