typescript-best-practices について
TypeScript を扱う AI エージェント向けのスキルで、型安全性を高め、コンパイル時にエラーを検出する TypeScript 5.x のパターンを示します。
主な機能
- 01厳格なコンパイラー設定
- 02unknown の使用と型の絞り込み
- 03型アサーションより satisfies を優先
- 04判別可能なユニオンで状態を表現
- 05switch の網羅性チェック
- 06ブランド型による ID の区別
活用シーン
- 01TypeScript コードの危険なパターンの確認
- 02型付き設定や API 状態の設計
- 03厳格な TypeScript プロジェクトの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ofershap/ typescript-best-practices --skill typescript-best-practicesインストール後、Claude Code で/typescript-best-practicesを入力して読み込まれたか確認します。
詳細
使用する場面
TypeScript コードを扱うときに、このスキルを使用してください。AI エージェントは、unknown の代わりに any を使う、satisfies の代わりに型アサーションを使う、判別可能なユニオンの代わりにオプショナルフィールドを使う、strict モードのオプションを設定し忘れるなど、古いパターンを頻繁に生成します。このスキルは、最新の TypeScript 5.x のパターンを適用します。
重要なルール
1. すべてのチェックを有効にして strict モードを設定する
誤り(エージェントがよく行う例):
{
"compilerOptions": {
"strict": false,
"target": "ES2020"
}
}
正しい例:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"target": "ES2022"
}
}
理由: strict モードを使うと、バグの大きなカテゴリ全体を検出できます。noUncheckedIndexedAccess は、安全でない配列やオブジェクトへのアクセスを防ぎます。エージェントは「手軽さ」を理由にこれらを省略しがちです。
2. 型アサーションの代わりに satisfies を使う
誤り(エージェントがよく行う例):
const config = {
port: 3000,
host: "localhost",
} as Config;
config.port.toFixed(); // No error even if port could be string
正しい例:
const config = {
port: 3000,
host: "localhost",
} satisfies Config;
config.port.toFixed(); // TypeScript knows port is number
理由: satisfies は型を拡大せずに検証します。as はコンパイラーのチェックを無効にし、バグを隠すことがあります。検証には satisfies を使い、as はコンパイラーよりも本当に多くのことを把握している場合にのみ使ってください。
3. オプショナルフィールドより判別可能なユニオンを使う
誤り(エージェントがよく行う例):
interface ApiResponse {
data?: User;
error?: string;
loading?: boolean;
}
正しい例:
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
理由: オプショナルフィールドでは、あり得ない状態(データとエラーの両方が存在する状態)が許容されます。判別可能なユニオンを使うと、それぞれの状態が明示され、網羅的にチェックできるようになります。
4. リテラル型には const アサーションを使う
誤り(エージェントがよく行う例):
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
};
// Type: { home: string; about: string; contact: string }
正しい例:
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
} as const;
// Type: { readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }
理由: as const がないと、TypeScript はリテラル型を string に拡大します。これを使うと、正確なリテラル型と読み取り専用プロパティを得られます。
5. any の代わりに unknown を使う
誤り(エージェントがよく行う例):
function parseJson(text: string): any {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // No error - runtime crash
正しい例:
function parseJson(text: string): unknown {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
if (isUser(data)) {
data.name; // Safe - type narrowed
}
理由: any はすべての型チェックを無効にします。unknown を使うと、型を絞り込んでから使用する必要があるため、コンパイル時にバグを検出できます。
6. 文字列パターンにはテンプレートリテラル型を使う
誤り(エージェントがよく行う例):
function getLocaleMessage(id: string): string { ... }
正しい例:
type Locale = 'en' | 'ja' | 'pt';
type MessageKey = 'welcome' | 'goodbye';
type LocaleMessageId = `${Locale}_${MessageKey}`;
function getLocaleMessage(id: LocaleMessageId): string { ... }
理由: テンプレートリテラル型を使うと、ユニオンから正確な文字列パターンを作成できます。コンパイラーがビルド時にタイプミスや無効な組み合わせを検出します。
7. 不要な推論を防ぐために NoInfer を使う
誤り(エージェントがよく行う例):
function createLight<C extends string>(colors: C[], defaultColor?: C) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // No error - purple widens C
正しい例:
function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // Error - 'purple' not in C
理由: NoInfer<T>(TypeScript 5.4 以降)は、パラメーターが型推論に影響するのを防ぎ、より厳密なチェックを実現します。
8. 型安全な ID にはブランド型を使う
誤り(エージェントがよく行う例):
function getUser(id: string): User { ... }
function getOrder(id: string): Order { ... }
const userId = getUserId();
getOrder(userId); // No error - but wrong!
正しい例:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
function getUser(id: UserId): User { ... }
function getOrder(id: OrderId): Order { ... }
const userId = getUserId();
getOrder(userId); // Error - UserId is not OrderId
理由: ブランド型を使うと、ある ID 型を別の ID 型が期待される場所に誤って渡すのを防げます。ブランドはコンパイル時にのみ存在し、実行時のコストはゼロです。
9. never を使った網羅的な switch を使う
誤り(エージェントがよく行う例):
function handleStatus(status: "active" | "inactive" | "pending") {
switch (status) {
case "active":
return "Active";
case "inactive":
return "Inactive";
// 'pending' silently falls through
}
}
正しい例:
function handleStatus(status: "active" | "inactive" | "pending") {
switch (status) {
case "active":
return "Active";
case "inactive":
return "Inactive";
case "pending":
return "Pending";
default: {
const _exhaustive: never = status;
throw new Error(`Unhandled status: ${_exhaustive}`);
}
}
}
理由: never チェックにより、ユニオンのすべてのメンバーが処理されていることを確認できます。新しいステータスが追加されると、コンパイラーが不足しているケースを指摘します。
10. 型アサーションより型述語を使う
誤り(エージェントがよく行う例):
function processItem(item: unknown) {
const user = item as User;
console.log(user.name);
}
正しい例:
function isUser(item: unknown): item is User {
return typeof item === "object" && item !== null && "name" in item && "email" in item;
}
function processItem(item: unknown) {
if (isUser(item)) {
console.log(item.name); // Safe - narrowed to User
}
}
理由: 型述語(item is User)は、実行時チェックによって型を安全に絞り込みます。型アサーション(as User)はコンパイラーを迂回し、バグを隠すことがあります。
11. 型のみのインポートには import type を使う
誤り(エージェントがよく行う例):
import { User, UserService } from "./user";
// User is only used as a type, but gets included in the bundle
正しい例:
import type { User } from "./user";
import { UserService } from "./user";
理由: import type はコンパイル時に削除されるため、バンドルサイズを削減できます。また、このインポートが型専用であることを明確にできます。
12. インデックスシグネチャより Record を使う
誤り(エージェントがよく行う例):
interface Config {
[key: string]: string;
}
正しい例:
type Config = Record<string, string>;
// Or better - use a specific union for keys:
type Config = Record<"host" | "port" | "env", string>;
理由: Record<K, V> はインデックスシグネチャよりも読みやすく、組み合わせて使いやすいものです。可能であれば、キーにユニオンを使うことで網羅的なチェックができます。
13. リソース管理には using を使う
誤り(エージェントがよく行う例):
const file = openFile("data.txt");
try {
processFile(file);
} finally {
file.close();
}
正しい例:
using file = openFile("data.txt");
processFile(file);
// file.close() called automatically via Symbol.dispose
理由: using キーワード(TypeScript 5.2 以降)は、Python の with や C# の using と同様に、Disposable プロトコルを通じて確実なリソースのクリーンアップを実現します。
パターン
- すべてのプロジェクトで
strict: trueとnoUncheckedIndexedAccess: trueを有効にします - 型を広げずに検証するには
satisfiesを使用します - 状態のモデリングには、
typeまたはkindフィールドを持つ判別可能なユニオンを使用します - 設定オブジェクトやルートマップには
as constを使用します - ドメイン固有の ID にはブランド型を使用します
- 型のみのインポートにはすべて
import typeを使用します - ユニオンの処理には、デフォルトケースで
neverを使う網羅的なswitchを使用します
アンチパターン
- 絶対に
anyを使用せず、unknownを使用して型ガードで絞り込みます - コンパイラーよりも確実に多くの情報を把握している場合を除き、型アサーションに
asを絶対に使用しません - 相互排他的な状態をモデル化するためにオプショナルフィールドを絶対に使用せず、判別可能なユニオンを使用します
- 理由を説明するコメントなしに
// @ts-ignoreや// @ts-expect-errorを絶対に使用しません enumは絶対に使用せず、代わりにas constオブジェクトまたはユニオン型を使用しますFunction型は絶対に使用せず、具体的な関数シグネチャを使用します- 手軽さを理由に厳格モードを絶対に無効にしません