typescript-strict について
厳格なTypeScriptの実践を支援するスキルで、信頼できないデータの扱い、型とスキーマの一元管理、コンパイラチェックを扱います。明確な契約と型安全性を重視する開発やコードレビューに適しています。
主な機能
- 01信頼できない境界でunknownを使用
- 02typeとinterfaceの選択指針
- 03スキーマと値集合の一元管理
- 04厳格なコンパイラチェックの推奨
- 05異なるプリミティブ型のブランド化
- 06信頼境界でのランタイム検証
活用シーン
- 01TypeScriptの危険なany使用のレビュー
- 02外部データ利用前の検証
- 03スキーマや値定義の重複回避
- 04同じ型の識別子や金額の区別
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill typescript-strictインストール後、Claude Code で/typescript-strictを入力して読み込まれたか確認します。
詳細
TypeScript Strict Mode
基本ルール
- 信頼できない境界では
unknownを使用します。 避けられないanyは外部宣言または相互運用用シムに限定し、その理由を説明します - 正当な理由なしに型アサーション(
as Type)を使用しません - リポジトリの
type/interfaceの規約に従います。 規約がない場合は、言語の意味論に基づいて選択します - すでに所有者がある値の集合は、二重に記述せず、そこから導出します。 リテラルユニオン、列挙値のリスト、固定値を対象とする
===/||の連鎖を書く前に、コードベースでその集合を検索します。型、as constリスト、スキーマですでに宣言されている場合は、そこから導出します
Type と Interface
type — ユニオン、タプル、マップ型、クローズドな別名
export type User = {
readonly id: string;
readonly email: string;
readonly name: string;
readonly roles: ReadonlyArray<string>;
};
型エイリアスは、ユニオン、交差型、タプル、プリミティブ、マップ型、オブジェクトの形状に名前を付けられます。宣言マージによって再オープンすることはできません。
interface — 拡張可能なオブジェクトの契約
export interface UserRepository {
findById(id: string): Promise<User | undefined>;
save(user: User): Promise<void>;
}
インターフェースはオブジェクトの形状を記述し、implements と連携し、競合をチェックしながら拡張でき、宣言マージをサポートします。振る舞いの契約や、意図的に開かれた拡張ポイントに役立ちますが、データの形状に使用することが禁じられているわけではありません。
スキーマの重複
所有する契約、バージョン、限定されたコンテキストごとにスキーマを一度定義し、その境界内でインポートします。フィールドが現在たまたま一致しているというだけで、独立してデプロイされるコンシューマーやコンテキストを結合しないでください。ドリフトが問題になる場合は、契約テストを用意し、各信頼境界で検証と変換を行います。
1つの境界内では、これは判断ではなく検索の問題です。コンパイル時の型とランタイムスキーマの両方で同じ固定値の集合が必要な場合は、1つの as const リストを唯一の所有元とし、両方をそこから導出します。その集合がすでに手書きのユニオンとして宣言されている場合は、その宣言を拡張してそこから導出します。スキーマ側にメンバーの重複を残さないでください。
// ✅ One owner; the type and the schema both derive from it
export const ORDER_STATES = ['draft', 'placed', 'shipped'] as const;
export type OrderState = (typeof ORDER_STATES)[number];
export const OrderStateSchema = z.enum(ORDER_STATES);
// ❌ Two owners for one set; they drift apart the first time a value is added
export type OrderState = 'draft' | 'placed' | 'shipped';
export const OrderStateSchema = z.enum(['draft', 'placed', 'shipped']);
// ✅ Define once for this API contract
export const CreateUserRequestSchema = z.object({
email: z.email(),
name: z.string().min(1),
});
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
// Import and use wherever needed
スキーマを配置する場所:信頼境界(HTTP ハンドラー、キューコンシューマー、ファイルや環境変数の解析、サードパーティ API のレスポンス)で検証し、その後は導出したプレーンな型を内部ロジックに渡します。内部関数は入力を信頼します。Standard Schema を実装するスキーマライブラリ(Zod 4+、Valibot、ArkType など)を優先し、検証ツールを交換可能な状態に保ちます。
Strict Mode の設定
tsconfig.json の設定
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"forceConsistentCasingInFileNames": true,
"allowUnusedLabels": false
}
}
各設定の機能
Strict の基本設定:
strict: true-noImplicitAnyやstrictNullChecksを含む、Strict 型チェック関連の設定を有効にします
プロジェクトに追加するチェック:
noUnusedLocals- 未使用のローカル変数をエラーにしますnoUnusedParameters- 未使用の関数パラメーターをエラーにしますnoImplicitReturns- すべてのコードパスで値を返さない場合にエラーにしますnoFallthroughCasesInSwitch- switch 文でのフォールスルーケースをエラーにします
コードベースに照らして評価する追加の安全性フラグ:
noUncheckedIndexedAccess- 配列やオブジェクトへのアクセス結果をT | undefinedにします(要素が存在すると仮定することによるランタイムエラーを防ぎます)exactOptionalPropertyTypes-property?: Tとproperty: T | undefinedを区別します(より正確な型になります)noPropertyAccessFromIndexSignature- インデックスシグネチャのプロパティへのアクセスにブラケット記法を必須にします(動的アクセスへの意識を促します)forceConsistentCasingInFileNames- オペレーティングシステム間での大文字と小文字の扱いに関する問題を防ぎますallowUnusedLabels- 未使用のラベルをエラーにします(何もしない誤ったラベルを検出します)
追加ルール
- 上流の型定義の不具合をまだ修正できない場合は、
@ts-ignoreよりも、妥当な理由を付けた範囲の狭い@ts-expect-errorを優先します - テストにもリポジトリの型安全性ポリシーを適用します。グローバル設定を緩めるのではなく、テスト専用の明示的な相互運用用シムを使用します
アーキテクチャ上の知見:noUnusedParameters は設計上の問題を検出します
noUnusedParameters ルールは、アーキテクチャ上の問題を明らかにすることがあります。
例:未使用のパラメーターがある関数は、そのパラメーターを別のレイヤーに配置すべきであることを示している場合があります。Strict モードを使えば、こうした設計上の問題を早期に検出できます。
不変性、純粋関数、コンポジション
不変性(readonly、ReadonlyArray)、純粋関数、コンポジション、Result 型、配列メソッド、ファクトリー関数に関する詳しいパターンについては、functional スキルを参照してください。これらはコードベース全体で使用されている標準的なパターンです。
TypeScript 固有の注意点:
- 不変性が契約の一部である場合、特に共有されるドメイン値には
readonlyとReadonlyArray<T>を使用します - コンパイラーが強制するのはプロパティの浅い不変性です。ネストされた実行時の値まで深く不変にはなりません
- 不変条件、ライフサイクル、プロジェクトの慣習に応じてファクトリーかクラスを選択します。依存性注入ではどちらの形式も必須ではありません
信頼境界ではスキーマを優先する
実行時スキーマが必要な場合
信頼できないデータが境界を越え、プログラムが使用前にその形状や制約を検証する必要がある場合は、実行時スキーマが必要です。一般的な兆候には、次のようなものがあります。
- HTTP、キュー、ファイル、環境、またはサードパーティーからのデータがシステムに入ってくる
- 独立してデプロイされるシステム間でデータ契約がやり取りされる
- 既存の本番用スキーマを再利用することで有用な根拠を追加できる、契約の形に沿ったテストフィクスチャ
内部不変条件には、デフォルトでスキーマは必要ありません。値が信頼された単一のプロセス内で作成・使用される場合は、スマートコンストラクター、ブランド型、またはドメイン値オブジェクトを所有者とするほうが明確なことがあります。
// API responses, user input, external data
const UserSchema = z.object({
id: z.uuid(),
email: z.email(),
});
type User = z.infer<typeof UserSchema>;
// Validate at boundary
const user = UserSchema.parse(apiResponse);
スキーマが不要な場合
- 純粋な内部型(ユーティリティ、状態)
- Result/Option 型(検証は不要)
- TypeScript のユーティリティ型(
Partial<T>、Pick<T>など) - 動作契約(インターフェース — 構造的であり、検証されません)
- コンポーネントの props(URL/API 由来の場合を除く)
// ✅ CORRECT - No schema needed
type Result<T, E> =
| { success: true; data: T }
| { success: false; error: E };
// ✅ CORRECT - Interface, no validation
interface UserService {
createUser(user: User): void;
}
ブランド型
基になる型が同じ2つの値を互いに取り違えて渡してもコンパイラーが検出できない場合は、プリミティブ型にブランドを付けます。たとえば、異なる種類の識別子、金額とレート、未加工の文字列と正規化された文字列などです。境界でそれぞれを検証するだけでは十分ではありません。解析後も値が string のままなら、下流のどこでも入れ替えたペアがコンパイルできてしまいます。それぞれの種類に固有のブランドを与え、値の検証を行う場所でのみそのブランドを付与し、プリミティブ型ではなくブランド型を使って、それらを受け取る関数を宣言します。
type UserId = string & { readonly brand: unique symbol };
type PaymentMinorUnits = number & { readonly brand: unique symbol };
type Currency = 'GBP' | 'USD' | 'EUR';
type PaymentMoney = {
readonly minorUnits: PaymentMinorUnits;
readonly currency: Currency;
};
// Type-safe at compile time
const processPayment = (userId: UserId, amount: PaymentMoney) => {
// Implementation
};
// ❌ Can't pass raw string/number
processPayment('user-123', 100); // Error
// ✅ Brand via a validating constructor — the ONE place an assertion is justified
const toUserId = (raw: string): UserId => {
if (raw.length === 0) throw new Error('UserId cannot be empty');
return raw as UserId;
};
const toPaymentMinorUnits = (raw: number): PaymentMinorUnits => {
if (!Number.isSafeInteger(raw) || raw <= 0) {
throw new Error('Payment minor units must be a positive safe integer');
}
return raw as PaymentMinorUnits;
};
const toPaymentMoney = (minorUnits: number, currency: Currency): PaymentMoney => ({
minorUnits: toPaymentMinorUnits(minorUnits),
currency,
});
processPayment(toUserId('user-123'), toPaymentMoney(2_500, 'GBP')); // £25.00
アプリケーションコード内に as UserId を散在させないでください。型アサーションはコンストラクター内(またはスキーマの transform 内)だけに置き、すべてのブランド型の値が検証済みであることを保証します。
上記の決済境界は、すでに丸められた整数の最小通貨単位を受け取るため、NaN、無限大、2進浮動小数点数の小数部分は拒否されます。境界で十進数の主要通貨単位のテキストを受け取る場合は、通貨の最小通貨単位の指数と明示的な丸めポリシーを使って解析するか、許容精度を超える値を拒否してください。Math.round(rawNumber * 100) は決して使用しないでください。
まとめのチェックリスト
TypeScript コードを書く際は、次の点を確認してください。
- 説明のつかない
anyがないこと。避けられない相互運用コードは範囲を絞り、局所化されていること - 正当な理由のない型アサーションがないこと
-
typeとinterfaceがリポジトリの慣習、または必要な言語仕様に従っていること - 各契約/バージョン/コンテキストにつき、スキーマの所有者が1つであること。独立した境界が利便性のために結合されていないこと
- コードベースですでに宣言されている固定値の集合は、二重に記述せず、そこから導出されていること
- 交換可能な、同じ型のプリミティブ(識別子、金額、コード)にブランドが付けられており、各ブランドが値の検証を行う場所でのみ付与されていること
-
strictが有効であり、追加のコンパイラーチェックがリポジトリで採用されているポリシーに従っていること - 不変性、純粋関数、コンポジションについては
functionalスキルを参照すること