enforcing-typescript-standards について
Enforcing TypeScript Standardsは、TypeScriptコードを扱うエージェント向けのスキルです。型、インポート、クラス構成、安全性、テスト、エラー処理の規則と、変更を確認・改善する手順を示します。
主な機能
- 01明示的な型指定と型の再利用
- 02インポートとクラスメンバーの整理
- 03安全性を高めるTypeScriptパターン
- 04エラー処理とテストの改善
- 05コードレビューとリファクタリングの指針
- 06ビルドとlintの確認
活用シーン
- 01.tsおよび.tsxファイルの作成・変更
- 02TypeScriptの変更が規則に沿うか確認
- 03プロジェクト標準に沿ったリファクタリング
- 04型チェックとlint結果の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jgeurts/ oxlint-config-decent --skill enforcing-typescript-standardsインストール後、Claude Code で/enforcing-typescript-standardsを入力して読み込まれたか確認します。
詳細
TypeScript 標準の適用
明示的な型指定、インポートの整理、クラスメンバーの順序、コードの安全性に関するルールなど、プロジェクトの中核となる TypeScript 標準を適用します。
トリガー
ユーザーが次のいずれかを発言、または暗に示した場合に、このスキルを有効にします。
- 「write」「create」「implement」「add」「build」(新しい TypeScript コード)
- 「fix」「update」「change」「modify」「refactor」(既存の TypeScript コード)
- 「review」「check」「improve」「clean up」(コード品質)
.tsまたは.tsxファイルに関するあらゆるリクエスト
具体的なトリガー:
- 新しい
.tsまたは.tsxファイルの作成 - 既存の TypeScript コードの変更
- TypeScript コードの準拠状況のレビュー
中核となる標準
型安全性
- 明示的な戻り値の型:実用的な場合は、明示的な戻り値の型を指定してください。型推論が明らかで、明確さが増さない場合は省略します
- メンバーのアクセス修飾子を明示:クラスメンバーには
public、private、またはprotectedが必要です - 型専用インポート:型には
import typeを使用します:import type { Foo } from './foo.js' - 型の構成要素をソート:ユニオン型と交差型はアルファベット順に並べる必要があります
- Error オブジェクトのみを throw:文字列やその他のプリミティブ型は決して throw しないでください
anyと型アサーションを避ける:anyやasキャストより、適切な型付けを優先してください。本当に必要な場合にのみ使用します- JSON フィールドを明示的に型指定:JSON データには
Record<string, unknown>または具体的なインターフェースを使用し、anyは決して使用しないでください - 変換には Number() を使用:
parseInt(value, 10)やparseFloat(value)よりNumber(value)を優先してください - 既存の型を再利用:新しいインターフェースを定義する前に、直接再利用、拡張、または
Pick、Omit、Partialなどのユーティリティ型で派生できる既存の型を検索してください
型アサーションの代替手段
as を使用する前に、次の方法を順番に試してください。
- ソースで適切に型付けする
- 型ガード(
typeof、instanceof) - 制御フローによる型の絞り込み
- カスタム型述語関数
- 判別可能なユニオン型
// Bad
const user = data as User;
// Good
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'id' in data;
}
if (isUser(data)) {
// data is now typed as User
}
インポートの整理
- インポート順:組み込み → 外部 → 内部 → 親 → 同階層 → index(各グループ内はアルファベット順)
- 重複インポートの禁止:同じモジュールからのインポートはまとめます
- インポート後の改行:インポートブロックの後には空行が必要です
クラスメンバーの順序
- シグネチャ(call/construct)
- フィールド:private → public → protected
- コンストラクター:public → protected → private
- メソッド:public → protected → private
コードスタイル
- 巧妙さよりシンプルさ:気の利いた一行コードより、簡潔で読みやすいコードを優先します
- 早期リターン:ガード節を使ってネストを減らし、エッジケースでは早期にリターンします
- Null 合体演算子:デフォルト値には
||より??を優先します(0や''での誤判定を避けられます) - オプショナルチェーン:安全なプロパティアクセスには
?.を使用します - 既存のパターンに合わせる:コードベースで既に確立されている規約に従います
- 意味のある識別子:名前は説明的である必要があります(例外:
_、i、j、k、e、x、y) - 関数宣言:
const foo = function()ではなくfunction foo()を使用します - const を優先:再代入が必要でない限り
constを使用します - var を禁止:常に
constまたはletを使用します - オブジェクトの短縮記法:
{ foo: foo }ではなく{ foo }を使用します - テンプレートリテラル:
'Hello ' + nameではなく`Hello ${name}`を使用します - 厳密等価:null の比較を除き、
===を使用します - 1 ファイルにつきクラス 1 つ:1 ファイル内のクラス定義は最大 1 つです
reduceを避ける:明確さを優先し、for...ofループや他の配列メソッドを使用します- クラスより関数:状態が必要な場合を除き、静的メソッドを持つクラスよりエクスポートされた関数を優先します
- ネストした関数を禁止:ヘルパー関数は他の関数内ではなく、モジュールレベルで定義します
- 不変性:既存のオブジェクトや配列を変更せず、新しいものを作成します
命名規則
- 列挙型メンバー:
PascalCaseを使用します(例:MyValue) - 末尾のアンダースコアを禁止:識別子の末尾に
_を付けることはできません
コメント
- 冗長なコメントを避ける:コードから明確に読み取れる内容には、決してコメントを付けないでください
- 重複するコメントを避ける:関数名、型、近くにあるコメントの情報を繰り返さないでください
- 意味のあるコメントのみ:「何をするか」ではなく「なぜそうするか」を説明するコメントだけを追加してください。コードを見れば何をするかは分かります
ブール式
- 真偽値チェックを優先する:明示的な比較より、暗黙の真偽値チェックを使用してください
- 例外:
0/''(有効な値)とnull/undefinedを区別することが意味的に重要な場合は、明示的なチェックを使用してください
テスト
- モックを最小限にする:何もかもモックにするのは避け、利用できる場合は実際の実装やデータジェネレーターを使用してください
- 実際の動作をテストする:モックのテストはあまり価値がありません。実際のコードパスをテストしてください
- 手を抜かない:正常系だけでなく、エッジケースも網羅する徹底したテストを書いてください
エラー処理
- 具体的なエラー型を使う:意味がある場合は、汎用的な
Errorより具体的なエラー型を優先してください - エラーを握りつぶさない:空のcatchブロックでエラーを握りつぶさないでください
- 拒否を処理する:Promiseの拒否は必ず処理してください
- エラーを伝播させる:再スローやログ出力だけを目的にエラーを捕捉しないでください。エラーハンドラーまで伝播させてください
負の知識
次のアンチパターンを避けてください。
- 本番コードでの
console.log()文 eval()またはFunction()コンストラクター- ネストした三項演算子
Promise.allを使えばより簡潔になるループ内のawait(順序が重要な場合や、並列化で複雑になる場合は逐次的なawaitで構いません)- 空のインターフェース
- 変数のシャドーイング
- ループ内で定義される関数
- 説明なしの
@ts-ignore(10文字以上の説明を付けた@ts-expect-errorを使用してください) - コードを言い換えるコメント:
counter++の上にある// increment counter - 型情報と重複するコメント:戻り値の型が
: stringなのに// returns a stringと書くこと - コメントアウトされたコード(削除してください。必要な場合はバージョン管理を使用してください)
- 冗長なブール値比較:
arr.length > 0、str !== ''、obj !== null && obj !== undefined - コメントでのリントルールの無効化(代わりにコードを修正してください)
any型やas型アサーションの多用- 実際の実装やデータジェネレーターを使わず、テストでモックを使いすぎること
- エラーを黙って握りつぶす空のcatchブロック
??のほうが適切な場面で、デフォルト値に||を使うこと- 早期リターンで簡潔にできる箇所の深いネスト
- 再スローやログ出力だけを目的としたエラーの捕捉
- 関数内での関数定義のネスト
- 新しいオブジェクトや配列を作らず、既存のものを変更すること
- TOCTOU:操作前にファイル/リソースの存在を確認すること(操作を試み、エラーを処理してください)
- 静的メソッドのみを持つクラス(代わりに通常の関数を使用してください)
- 既存のインターフェースを再利用したり、
Pick/Omit/Partialで派生させたりせずに、インターフェースを重複定義すること
検証ワークフロー
- 分析:コードの変更をこれらのTypeScript標準と照らし合わせて確認します
- 生成/リファクタリング:上記のすべてのルールに準拠するよう、コードを作成または変更します
- 簡素化:簡素化できる箇所を見直します。巧妙な解決策より、明確で分かりやすいコードを優先してください
- 命名を確認:変更後も、変数名と関数名が文脈に合っていることを確認します
- ビルド:型エラーなしでコンパイルできることを確認します(例:
pnpm run buildまたはpnpm exec tsc --noEmit) - リント:タスクを完了する前に、
pnpm run lintを実行して準拠していることを確認します
例
コメントの例
// Standard
// Retry with exponential backoff to handle transient network failures
async function fetchWithRetry(url: string, attempts = 3): Promise<Response> {
for (let i = 0; i < attempts; i++) {
try {
return await fetch(url);
} catch {
await sleep(2 ** i * 100);
}
}
throw new Error(`Failed after ${attempts} attempts`);
}
// Non-Standard
/**
* Fetches data from a URL with retry logic
* @param url - The URL to fetch from
* @param attempts - Number of attempts (default 3)
* @returns A Promise that resolves to a Response
*/
async function fetchWithRetry(url: string, attempts = 3): Promise<Response> {
// Loop through attempts
for (let i = 0; i < attempts; i++) {
try {
// Try to fetch the URL
return await fetch(url);
} catch {
// Wait before retrying
await sleep(2 ** i * 100);
}
}
// Throw error if all attempts fail
throw new Error(`Failed after ${attempts} attempts`);
}
ブール式の例
// Standard
if (myArray.length) {
}
if (myString) {
}
if (myObject) {
}
if (!value) {
}
// Non-Standard
if (myArray.length !== 0) {
}
if (myArray.length > 0) {
}
if (myString !== '') {
}
if (myObject !== null && myObject !== undefined) {
}
if (value === null || value === undefined) {
}
早期リターンの例
// Standard
function processUser(user: User | null): Result {
if (!user) {
return { error: 'No user provided' };
}
if (!user.isActive) {
return { error: 'User is inactive' };
}
return { data: transform(user) };
}
// Non-Standard
function processUser(user: User | null): Result {
if (user) {
if (user.isActive) {
return { data: transform(user) };
} else {
return { error: 'User is inactive' };
}
} else {
return { error: 'No user provided' };
}
}
クラスより関数を使う例
// Standard
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
export function formatCurrency(amount: number): string {
return `$${amount.toFixed(2)}`;
}
// Non-Standard
export class Calculator {
static calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
static formatCurrency(amount: number): string {
return `$${amount.toFixed(2)}`;
}
}
関数のネストを避ける例
// Standard
function transformItem(item: Item): TransformedItem {
return { id: item.id, name: item.name.toUpperCase() };
}
async function processItems(items: Item[]): Promise<TransformedItem[]> {
return items.map(transformItem);
}
// Non-Standard
async function processItems(items: Item[]): Promise<TransformedItem[]> {
function transformItem(item: Item): TransformedItem {
return { id: item.id, name: item.name.toUpperCase() };
}
return items.map(transformItem);
}
不変性の例
// Standard
function addItem(items: Item[], newItem: Item): Item[] {
return [...items, newItem];
}
function removeItem(items: Item[], id: string): Item[] {
return items.filter((item) => item.id !== id);
}
function updateItem(items: Item[], id: string, updates: Partial<Item>): Item[] {
return items.map((item) => (item.id === id ? { ...item, ...updates } : item));
}
// Non-Standard
function addItem(items: Item[], newItem: Item): Item[] {
items.push(newItem);
return items;
}
function removeItem(items: Item[], id: string): Item[] {
const index = items.findIndex((item) => item.id === id);
items.splice(index, 1);
return items;
}
エラー伝播の例
// Standard
async function getUser(id: string): Promise<User> {
return userService.findById(id);
}
// Non-Standard
async function getUser(id: string): Promise<User> {
try {
return await userService.findById(id);
} catch (error) {
console.error(error);
throw error;
}
}
TOCTOUの例
// Standard
async function readConfig(path: string): Promise<Config> {
try {
const content = await readFile(path, 'utf-8');
return JSON.parse(content);
} catch (error) {
if (isNotFoundError(error)) {
return defaultConfig;
}
throw error;
}
}
// Non-Standard
async function readConfig(path: string): Promise<Config> {
if (await fileExists(path)) {
const content = await readFile(path, 'utf-8');
return JSON.parse(content);
}
return defaultConfig;
}
型の再利用例
// Given an existing type
interface User {
id: string;
email: string;
name: string;
passwordHash: string;
createdAt: Date;
updatedAt: Date;
}
// Standard - derive from existing type
type PublicUser = Omit<User, 'passwordHash'>;
type UserSummary = Pick<User, 'id' | 'name'>;
type UserUpdate = Partial<Pick<User, 'email' | 'name'>>;
// Non-Standard - duplicating fields that already exist
interface PublicUser {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
interface UserSummary {
id: string;
name: string;
}
interface UserUpdate {
email?: string;
name?: string;
}
コメント
Markdown 対応