typescript-dry-principle
TypeScriptプロジェクトにおいて、DRY原則(Don't Repeat Yourself)を適用し、包括的なリファクタリングパターンを使用してコード重複を排除します。このスキルにより、保守性の高い再利用可能なコード構造を実現でき、開発効率の向上とバグ発生のリスク低減が可能です。共通ロジックの抽出、汎用的な関数・クラスの設計、モジュール化の推進などを通じて、プロジェクト全体のコード品質を向上させることができます。
description の原文を見る
Apply DRY principle to eliminate code duplication in TypeScript projects with comprehensive refactoring patterns
SKILL.md 本文
機能について
TypeScript プロジェクトでコードの重複を排除し、DRY(Don't Repeat Yourself)原則を適用するお手伝いをします:
- コードベースの分析: TypeScript ファイルをスキャンして、繰り返されるコードパターン、ロジック、型、設定を特定します
- 重複パターンの特定: 以下のような一般的なアンチパターンを検出します:
- 複数の関数/コンポーネント間のロジックの重複
- 型定義の繰り返し
- コピー&ペーストされたコードブロック
- 若干の変動を持つ類似関数
- 分散した設定値
- 共通ロジックの抽出: 重複したコードを再利用可能なユーティリティ関数とモジュールにリファクタリングします
- 型定義の統合: 重複した型を共有インターフェースと型ユーティリティにマージします
- 汎用ソリューションの作成: TypeScript ジェネリクスを使用して、型安全で再利用可能なコンポーネントを構築します
- フォルダ構造の整理: コードを論理的なディレクトリに再構成します(types/、utils/、constants/、hooks/、services/)
- 重複コードの置き換え: ファイルを更新して、コードをコピーする代わりに共有モジュールからインポートするようにします
- リファクタリングの検証: 変更後にコードがコンパイルされ、テストが成功することを確認します
使用するべき場合
このワークフローは次の場合に使用してください:
- 複数の TypeScript ファイルで類似したコードブロックに気付いた場合
- モジュールまたはコンポーネント間でコードをコピー&ペーストしている場合
- 型定義がファイル全体で重複または繰り返されている場合
- ビジネスロジックが若干の変動を伴って複数の場所に出現する場合
- 設定値が複数のファイルに分散している場合
- テストに重複したセットアップ/ティアダウンロジックが含まれている場合
- コード保守性を向上させ、技術負債を減らしたい場合
- 技術負債に対処するためにコードレビューの準備をしている場合
- 適切なコード組織を備えた新しい TypeScript プロジェクトをセットアップしている場合
リファクタリングのスコープが明確でない場合、または特定の領域に焦点を当てたい場合は、遠慮なく質問してください。
前提条件
- ソースコード(.ts、.tsx ファイル)を含む TypeScript プロジェクト
- TypeScript ファイルを読み取り、変更するためのファイル権限
- インストールされて設定された TypeScript コンパイラ
- (オプション)リファクタリングが機能を破壊しないことを確認するテストスイート
- (オプション)リファクタリング変更をコミットするための Git リポジトリ
ステップ
ステップ 1:重複パターンのためのコードベース分析
TypeScript ファイルをスキャンして重複を特定します:
# TypeScript ファイルを検索
find . -name "*.ts" -o -name "*.tsx" -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*"
# 一般的なパターンのためのファイル分析
# 以下を探してください:
# - 変動を伴う類似関数名(getUserData、getUserInfo、getUserDetails)
# - 繰り返される API 呼び出しまたはデータ取得ロジック
# - 複数のファイルで定義された重複型
# - わずかな違いを持つ類似コンポーネント構造
一般的な重複インジケーター:
- 類似した関数名(getUser vs getUserData vs getUserInfo)
- わずかな変動を持つほぼ同一のコードブロック
- 複数のファイルで定義された同じ型インターフェース
- 繰り返される検証または変換ロジック
- わずかな違いを持つ類似コンポーネント構造
ステップ 2:重複タイプの分類
適切なリファクタリングパターンを適用するための重複タイプを特定します:
| 重複タイプ | 説明 | リファクタリングアプローチ |
|---|---|---|
| ロジック重複 | 複数の関数に存在する同じビジネスロジック | 共有ユーティリティ関数に抽出 |
| 型重複 | ファイル全体で重複するインターフェース/型 | 共有 types/ ディレクトリに統合 |
| コンポーネント重複 | わずかな変動を持つ類似コンポーネント | TypeScript ジェネリクスを使用して汎用コンポーネントを作成 |
| 設定重複 | 複数のファイルに存在する同じ設定値 | constants/ ディレクトリを作成 |
| API 呼び出し重複 | 類似ロジックを持つ繰り返される API 呼び出し | API サービスレイヤーを作成 |
| 検証重複 | 複数の場所に存在する同じ検証ロジック | 共有バリデーターを作成 |
| テンプレート重複 | テンプレート化できるような類似コードパターン | 高階関数またはコンポーネントを作成 |
ステップ 3:共通ロジックをユーティリティ関数に抽出
重複したロジックを共有ユーティリティ関数にリファクタリングします:
例 1:データ変換ロジック
リファクタリング前(複数のファイルで重複):
// file1.ts の場合
function formatUserName(firstName: string, lastName: string): string {
return `${firstName.charAt(0).toUpperCase()}${firstName.slice(1).toLowerCase()} ${lastName.charAt(0).toUpperCase()}${lastName.slice(1).toLowerCase()}`
}
// file2.ts の場合
function formatAuthorName(firstName: string, lastName: string): string {
return `${firstName.charAt(0).toUpperCase()}${firstName.slice(1).toLowerCase()} ${lastName.charAt(0).toUpperCase()}${lastName.slice(1).toLowerCase()}`
}
リファクタリング後(共有ユーティリティに抽出):
// utils/stringUtils.ts の場合
export function capitalizeFirstLetter(word: string): string {
if (!word) return ''
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
}
export function formatFullName(firstName: string, lastName: string): string {
return `${capitalizeFirstLetter(firstName)} ${capitalizeFirstLetter(lastName)}`
}
// file1.ts の場合
import { formatFullName } from '../utils/stringUtils'
function formatUserName(firstName: string, lastName: string): string {
return formatFullName(firstName, lastName)
}
// file2.ts の場合
import { formatFullName } from '../utils/stringUtils'
function formatAuthorName(firstName: string, lastName: string): string {
return formatFullName(firstName, lastName)
}
例 2:API 呼び出し重複
リファクタリング前(複数のコンポーネントで重複):
// component1.ts の場合
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('Failed to fetch user')
return response.json()
}
// component2.ts の場合
async function fetchUserProfile(id: string): Promise<UserProfile> {
const response = await fetch(`/api/users/${id}/profile`)
if (!response.ok) throw new Error('Failed to fetch user profile')
return response.json()
}
リファクタリング後(共有サービスに抽出):
// services/apiService.ts の場合
class ApiService {
private baseUrl: string = '/api'
async fetch<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`)
if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`)
return response.json()
}
async getUser(id: string): Promise<User> {
return this.fetch<User>(`/users/${id}`)
}
async getUserProfile(id: string): Promise<UserProfile> {
return this.fetch<UserProfile>(`/users/${id}/profile`)
}
}
export const apiService = new ApiService()
// component1.ts の場合
import { apiService } from '../services/apiService'
async function fetchUser(id: string): Promise<User> {
return apiService.getUser(id)
}
// component2.ts の場合
import { apiService } from '../services/apiService'
async function fetchUserProfile(id: string): Promise<UserProfile> {
return apiService.getUserProfile(id)
}
ステップ 4:型定義の統合
重複する型定義を共有インターフェースにマージします:
リファクタリング前(複数のファイルで重複する型):
// file1.ts の場合
interface UserData {
id: string
name: string
email: string
}
// file2.ts の場合
interface UserInfo {
id: string
fullName: string
emailAddress: string
}
// file3.ts の場合
interface UserProfile {
userId: string
displayName: string
contactEmail: string
}
リファクタリング後(共有型ファイルに統合):
// types/user.ts の場合
export interface User {
id: string
name: string
email: string
}
// file1.ts の場合
import type { User } from '../types/user'
const userData: User = { /* ... */ }
// file2.ts の場合
import type { User } from '../types/user'
const userInfo: User = { /* ... */ }
// file3.ts の場合
import type { User } from '../types/user'
const userProfile: User = { /* ... */ }
高度な型統合(ユーティリティ型の使用):
// types/api.ts の場合
export type ApiResponse<T> = {
data: T
error: string | null
status: 'success' | 'error'
}
export type PaginatedResponse<T> = {
items: T[]
total: number
page: number
pageSize: number
}
// types/common.ts の場合
export type Optional<T> = T | null | undefined
export type Nullable<T> = T | null
export type DeepPartial<T> = {
[P in keyof T]?: T[P]
}
ステップ 5:TypeScript ジェネリクスを使用して汎用コンポーネントを作成
類似したコンポーネントを再利用可能な汎用コンポーネントにリファクタリングします:
例 1:汎用リストコンポーネント
リファクタリング前(重複したリストコンポーネント):
// UserList.tsx の場合
interface UserListProps {
users: User[]
onSelectUser: (user: User) => void
}
export function UserList({ users, onSelectUser }: UserListProps) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onSelectUser(user)}>
{user.name}
</li>
))}
</ul>
)
}
// ProductList.tsx の場合
interface ProductListProps {
products: Product[]
onSelectProduct: (product: Product) => void
}
export function ProductList({ products, onSelectProduct }: ProductListProps) {
return (
<ul>
{products.map(product => (
<li key={product.id} onClick={() => onSelectProduct(product)}>
{product.name}
</li>
))}
</ul>
)
}
リファクタリング後(汎用コンポーネントに抽出):
// components/GenericList.tsx の場合
interface GenericListProps<T> {
items: T[]
key: keyof T
renderItem: (item: T) => React.ReactNode
onSelectItem: (item: T) => void
}
export function GenericList<T>({ items, key, renderItem, onSelectItem }: GenericListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={String(item[key])} onClick={() => onSelectItem(item)}>
{renderItem(item)}
</li>
))}
</ul>
)
}
// UserList.tsx の場合
import { GenericList } from './GenericList'
import type { User } from '../types/user'
export function UserList({ users, onSelectUser }: { users: User[]; onSelectUser: (user: User) => void }) {
return (
<GenericList
items={users}
key="id"
renderItem={(user) => <span>{user.name}</span>}
onSelectItem={onSelectUser}
/>
)
}
// ProductList.tsx の場合
import { GenericList } from './GenericList'
import type { Product } from '../types/product'
export function ProductList({ products, onSelectProduct }: { products: Product[]; onSelectProduct: (product: Product) => void }) {
return (
<GenericList
items={products}
key="id"
renderItem={(product) => <span>{product.name}</span>}
onSelectItem={onSelectProduct}
/>
)
}
例 2:汎用データ取得フック
リファクタリング前(複数のコンポーネントで重複する取得ロジック):
// useUserData.ts の場合
export function useUserData(userId: string) {
const [data, setData] = useState<User | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function fetchData() {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`)
const result = await response.json()
setData(result)
} catch (err) {
setError('Failed to fetch user')
} finally {
setLoading(false)
}
}
fetchData()
}, [userId])
return { data, loading, error }
}
リファクタリング後(汎用フックに抽出):
// hooks/useApiData.ts の場合
interface UseApiDataOptions {
immediate?: boolean
}
export function useApiData<T>(url: string, options: UseApiDataOptions = {}) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function fetchData() {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
const result = await response.json()
setData(result)
} catch (err) {
setError('Failed to fetch data')
} finally {
setLoading(false)
}
}
if (options.immediate !== false) {
fetchData()
}
}, [url, options.immediate])
return { data, loading, error, refetch: () => fetchData() }
}
// useUserData.ts の場合
export function useUserData(userId: string) {
return useApiData<User>(`/api/users/${userId}`)
}
ステップ 6:constants ディレクトリを作成
分散している設定値を共有定数に抽出します:
リファクタリング前(複数のファイルに分散した設定):
// component1.tsx の場合
const API_BASE_URL = 'https://api.example.com/v1'
const MAX_RETRIES = 3
const TIMEOUT = 5000
// component2.tsx の場合
const API_BASE_URL = 'https://api.example.com/v1'
const MAX_RETRIES = 3
const TIMEOUT = 5000
// service.ts の場合
const API_BASE_URL = 'https://api.example.com/v1'
const MAX_RETRIES = 3
const TIMEOUT = 5000
リファクタリング後(定数に統合):
// constants/api.ts の場合
export const API_CONFIG = {
BASE_URL: 'https://api.example.com/v1',
MAX_RETRIES: 3,
TIMEOUT: 5000,
ENDPOINTS: {
USERS: '/users',
PRODUCTS: '/products',
ORDERS: '/orders'
} as const
} as const
export const UI_CONFIG = {
ANIMATION_DURATION: 300,
DEBOUNCE_DELAY: 500,
TOAST_DURATION: 3000
} as const
// component1.tsx の場合
import { API_CONFIG } from '../constants/api'
async function fetchData() {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.USERS}`)
// ...
}
// component2.tsx の場合
import { API_CONFIG } from '../constants/api'
async function fetchData() {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.PRODUCTS}`)
// ...
}
ステップ 7:バリデーターユーティリティを作成
重複する検証ロジックを共有バリデーターに抽出します:
例:メール検証
リファクタリング前(複数の場所で重複する検証):
// component1.tsx の場合
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
// component2.tsx の場合
function checkEmail(email: string): boolean {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailPattern.test(email)
}
// form.tsx の場合
function isEmailValid(email: string): boolean {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return pattern.test(email)
}
リファクタリング後(バリデーターに統合):
// utils/validators.ts の場合
export class Validators {
private static emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
static email(email: string): boolean {
return this.emailRegex.test(email)
}
static minLength(value: string, min: number): boolean {
return value.length >= min
}
static maxLength(value: string, max: number): boolean {
return value.length <= max
}
static required(value: string): boolean {
return value.trim().length > 0
}
static pattern(value: string, pattern: RegExp): boolean {
return pattern.test(value)
}
static phone(value: string): boolean {
const phoneRegex = /^\+?[\d\s-()]+$/
return phoneRegex.test(value)
}
}
// component1.tsx の場合
import { Validators } from '../utils/validators'
function validateEmail(email: string): boolean {
return Validators.email(email)
}
// form.tsx の場合
import { Validators } from '../utils/validators'
function checkEmail(email: string): boolean {
return Validators.email(email)
}
ステップ 8:フォルダ構造を整理
コードを論理的なディレクトリに再構成します:
src/
├── types/ # 共有型定義
│ ├── index.ts # 型エクスポート
│ ├── user.ts # ユーザー関連型
│ ├── api.ts # API レスポンス型
│ └── common.ts # 一般的なユーティリティ型
├── utils/ # 再利用可能なユーティリティ関数
│ ├── stringUtils.ts # 文字列操作
│ ├── dateUtils.ts # 日付フォーマット
│ ├── validators.ts # 検証ロジック
│ └── apiHelpers.ts # API ヘルパー関数
├── constants/ # 設定値
│ ├── api.ts # API エンドポイントと設定
│ └── ui.ts # UI 設定
├── services/ # API とビジネスロジックサービス
│ └── apiService.ts # API サービスレイヤー
├── components/ # 再利用可能な UI コンポーネント
│ ├── generic/ # 汎用コンポーネント
│ └── specific/ # ドメイン固有のコンポーネント
├── hooks/ # カスタム React フック
│ ├── useApiData.ts # API データ取得フック
│ └── useAuth.ts # 認証フック
└── pages/ # ページコンポーネント
ステップ 9:重複コードを import に置き換え
ファイルを更新して、コードを複製する代わりに共有モジュールを使用するようにします:
// リファクタリング前 - 重複ロジック
function calculateTotal(items: any[]): number {
let total = 0
for (const item of items) {
total += item.price
}
return total
}
function calculateSum(items: any[]): number {
let sum = 0
for (const item of items) {
sum += item.amount
}
return sum
}
function calculateAverage(items: any[]): number {
let sum = 0
for (const item of items) {
sum += item.value
}
return sum / items.length
}
// リファクタリング後 - 共有ユーティリティに抽出
import { calculateArraySum } from '../utils/arrayUtils'
function calculateTotal(items: any[]): number {
return calculateArraySum(items, 'price')
}
function calculateSum(items: any[]): number {
return calculateArraySum(items, 'amount')
}
function calculateAverage(items: any[]): number {
return calculateArraySum(items, 'value') / items.length
}
共有ユーティリティ関数:
// utils/arrayUtils.ts の場合
export function calculateArraySum<T>(items: T[], key: keyof T): number {
return items.reduce((sum, item) => sum + (item[key] as number), 0)
}
export function calculateArrayAverage<T>(items: T[], key: keyof T): number {
if (items.length === 0) return 0
const sum = calculateArraySum(items, key)
return sum / items.length
}
ステップ 10:リファクタリングを検証
リファクタリング後にコードがコンパイルされ、テストが成功することを確認します:
# TypeScript コンパイルをチェック
npx tsc --noEmit
# テストを実行
npm run test
# プロジェクトをビルド
npm run build
# リンティングを実行
npm run lint
リファクタリング検証チェックリスト:
- TypeScript コンパイルエラーがない
- すべてのテストが合格
- 新しいリンティングエラーが導入されていない
- 特定されたすべての重複コードを削除
- 共有モジュールが適切にエクスポートされている
- インポートが正しい相対/絶対パスを使用している
- フォルダ構造が論理的に整理されている
ベストプラクティス
DRY 原則:
- 単一責任: 各関数/モジュールには 1 つの明確な目的を持つべきです
- 構成より継承: コードの再利用には継承より構成を優先します
- 不変性: 可能な限り不変のデータ構造を使用します
- 型安全性: TypeScript の型システムを活用してランタイムエラーを防ぎます
- 早期抽出: 重複を特定したらすぐにリファクタリングします
- ユーティリティファースト: ビジネスロジックの前に再利用可能なユーティリティを作成します
TypeScript 固有のベストプラクティス:
- インターフェース vs 型: オブジェクト形状にはインターフェース、共用体/プリミティブには型を使用します
- ユーティリティ型: Pick、Omit、Partial、Record を型変換に使用します
- ジェネリクス: 再利用可能なコンポーネントと関数にジェネリクスを使用します
- 型ガード: ランタイム型チェック用の型ガードを使用します
- Never 型:
anyを避けます。適切な型定義を使用します - Readonly: 適切な場所でプロパティを readonly としてマークします
コード組織化:
- 機能ベースのフォルダ: 関連ファイルを機能ディレクトリでグループ化します
- 共有リソース: 共有ユーティリティを専用ディレクトリに保持します
- インデックスファイル: index.ts ファイルを使用してクリーンなインポートを実現します
- バレルエクスポート: 単一のインデックスファイルから関連項目をエクスポートします
- 関心の分離: UI、ビジネスロジック、データを分離したままにします
リファクタリングワークフロー:
- まず分析: 変更を加える前に、すべての重複を特定します
- 小さなステップ: インクリメンタルにリファクタリングし、各変更後にテストします
- テストカバレッジ: リファクタリングされたコードに対してテストが存在することを確認します
- Git コミット: リファクタリングを論理的なチャンクでコミットします
- 後方互換性: 既存のパブリック API を維持します
一般的な問題
リファクタリング後の破壊的な変更
問題: リファクタリングにより、リファクタリングされたモジュールをインポートする既存のコードが破損します
解決策:
- git bisect を使用して、何がコードを破損させたかを特定します
- インポートパスをチェックし、正しいことを確認します
- エクスポートされたインターフェースがコンシューマーが期待するものと一致することを確認します
- 適切な再エクスポートを備えた index.ts ファイルを追加します
- リファクタリング中に頻繁にテストを実行します
循環依存
問題: 抽出されたユーティリティが循環依存を作成します
解決策:
- 抽出前に依存グラフを分析します
- ユーティリティをより小さく、より焦点を絞ったモジュールに分割します
- 必要に応じて依存性注入を使用します
- 密接に関連するユーティリティのマージを検討します
- 共有型を別の types/ ディレクトリに移動します
統合後の型エラー
問題: マージされた型が TypeScript コンパイルエラーを引き起こします
**解
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- majiayu000
- ライセンス
- Apache-2.0
- 最終更新
- 2026/5/9
Source: https://github.com/majiayu000/claude-skill-registry-data / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。