0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発用 コーディング規約(TypeScript / React / Next.js)

0
Posted at

はじめに

個人開発で迷うポイントを減らすための、自分用コーディングルール。

他の記事やChatGPTの回答を参考にしつつ、まだ整理できていない部分もある。
開発を進める中で必要に応じて内容を見直し、継続的にブラッシュアップしていく。

変数・定数・値の扱い

let,varは原則使わない
理由
値の再代入を防ぎコードの意図が明確になる
・「途中で値が変わるかもしれないという読み手の不安を減らす
予期しない動作が起きづらくなる
  • 変数の宣言にはconstを使用する
  • 値が本当に変わる必要がある場合のみletを検討する
ハードコーディング/マジックナンバー禁止
理由
意味が不明瞭でコードの可読性を下げる
意図が変わった場合に修正箇所を特定しづらくバグの温床になる
同じ数字を別の意味で再利用すると混乱の原因になる
  • 数値は定数やenumに名前を付けて管理する
bad
// ボタンの横幅を 200px に固定している → 数字の意味が不明
const MyButton = () => {
  return <button style={{ width: 200 }}>Click me</button>;
};

// ロジック内で 7 を使っている → 何の意味の 7 かわからない
const isLucky = (num: number) => {
  return num % 7 === 0;
};

good
// 定数に名前をつけて意味を明確に
const BUTTON_WIDTH_PX = 200;

const MyButton = () => {
  return <button style={{ width: BUTTON_WIDTH_PX }}>Click me</button>;
};

// マジックナンバーを定数化して意図を明確に
const LUCKY_NUMBER = 7;

const isLucky = (num: number) => {
  return num % LUCKY_NUMBER === 0;
};

型安全

anyは使用禁止
理由
TypeScriptの型チェックが効かず実質JavaScriptと同じになる
実行しないと失敗が分からないため保守性が低下する
  • 必要な型は必ず自分で定義する
  • どうしても避けられない場合のみ限定的に許容する(コメントで理由を書く)
bad
const fetchUser = async (): Promise<any> => {
  const res = await fetch("/api/user");
  return res.json(); // 返り値の構造が不明
};
good
type User = {
  id: string;
  name: string;
  email: string;
};

const fetchUser = async (): Promise<User> => {
  const res = await fetch("/api/user");
  const json = (await res.json()) as User; // 明確に User 型にする
  return json;
};
型アサーション(as)の乱用禁止
理由
型安全性が失われる
TypeScriptの型推論を無視するため保守性が下がる
  • まずはデータ構造と型定義を整理して回避できないか見直す
  • as constはOK(readonly化のため)
  • 回避できない場合はユーザ定義型ガードなどで安全に型を絞る
bad
// unknown な値をそのまま User と決めつけてしまう
const json = await fetch('/api/user').then(res => res.json());
const user = json as User; // 本当は User じゃなくても通る

// 実行時に user.name が undefined なら落ちる危険性
console.log(user.name.toUpperCase());
good
type User = { id: number; name: string };

function isUser(v: unknown): v is User {
  return (
    typeof v === "object" &&
    v !== null &&
    "id" in v &&
    "name" in v &&
    typeof (v as any).id === "number" &&
    typeof (v as any).name === "string"
  );
}

const data: unknown = await fetch("/api/user").then(r => r.json());

if (isUser(data)) {
  console.log(data.name); // 安全
} else {
  console.error("Invalid data");
}
型定義は基本的にtypeを使う(type vs interface)
理由
開発体験の良さ
  • 基本的にはtypeでもinterfaceでも問題ない
  • プロジェクトで統一すると可読性・保守性が向上する
  • VSCodeでホバーしたときの表示の分かりやすさはtypeの方が優れている
  • ライブラリ開発の場合は、利用者が拡張しやすいinterfaceを選ぶと良い
null,undefinedは型で表現する(optional ? を使う)
理由
TypeScriptでは型で値の存在非存在を表現できる
  • Optionalな値には undefined を直接使わず、TypeScriptの ? を活用
bad
// optionalの値にundefinedを直接割り当てている
interface User {
  name: string;
  age: number | undefined;
}

const user: User = {
  name: "Alice",
  age: undefined, // 直接undefinedを使用している
};
good
// Optionalな値には?を使う
interface User {
  name: string;
  age?: number; // ageが無くてもOK
}

const user1: User = {
  name: "Alice",
};

const user2: User = {
  name: "Bob",
  age: 25, // ageがある場合だけ値を持つ
};

関数・ロジック設計

関数は副作用(例:state更新)を減らして純粋性を保つ
理由
再利用性
可読性
テスト容易性
  • 1関数1役割を目指す
  • APIリクエストを行う関数は「引数を受け取り、レスポンスを返す」だけの純粋関数にする。
  • useStateなどの副作用は関数の外側で管理する。
  • データ整形は別関数に分ける
bad
// components/MyComponentBad.tsx
const MyComponent = () => {
  const [data, setData] = useState<any>(null)

  const handleSubmit = async (formValues: any) => {
    // データ整形とAPI呼び出しを一緒にやる
    const body = {
      payload: formValues,
      timestamp: new Date().toISOString()
    }

    try {
      // レスポンスからデータを直接取り出す
      const response = await axios.request({
        url: '/example-endpoint',
        method: 'post',
        data: body,
      })
      setData(response.data)
    } catch (err) {
      console.error('API取得エラー:', err)
    }
  }
good
// File: apiHelper.ts
// フォームデータをAPI用に変換する関数
export const formatRequestData = (formValues: any) => {
  return {
    // 必要に応じて変換
    payload: formValues,
    timestamp: new Date().toISOString(),
  }
}

// APIを叩き、レスポンスのみを返す関数
export const getApiData = async (formValues: any) => {
  const body = formatRequestData(formValues)
  try {
    const response = await axios.request({
      url: '/example-endpoint',
      method: 'post',
      data: body,
    })
    return response.data
  } catch (err) {
    console.error('API取得エラー:', err)
    return null
  }
}

// MyComponent.tsx
const MyComponent = () => {
  const [data, setData] = useState<any>(null)

  const handleSubmit = async (formValues: any) => {
    const result = await getApiData(formValues)
    setData(result)
  }

  return (
    <div>
      <button onClick={() => handleSubmit({ key: 'value' })}>
        データ取得
      </button>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}
早期リターン推奨
理由
ネストを減らせる
可読性
保守性
  • 関数の冒頭で「無効値チェック」や「早期終了条件」を先に処理する
  • メイン処理はできるだけネスト無しで書く
bad
// 配列の最大値を取得する関数
const getMaxValue = (arr: number[] | null): number => {
  // ネストが深くて読みにくい
  if (arr) {
    if (arr.length > 0) {
      let max = arr[0];
      for (let i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
          max = arr[i];
        }
      }
      return max;
    } else {
      return -Infinity; // 空配列の場合
    }
  } else {
    return -Infinity; // null の場合
  }
};
good
// 配列の最大値を取得する関数
const getMaxValue = (arr: number[] | null): number => {
  // ガード節で早期 return
  if (!arr || arr.length === 0) return -Infinity;

  // メイン処理はネストなしでシンプル
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) {
      max = arr[i];
    }
  }
  return max;
};
安易な共通化を避け、変わらない概念だけを抽出する
理由
似ている処理を無理にまとめると条件分岐だらけの巨大な関数 が生まれ把握困難になる
要件変更やデザイン変更時に壊れやすく保守性が落ちる
安定して変わらない概念だけを共通化するべき
  • 似ていてもまずは別々に実装
  • 変わらないことが明確になった“小さな概念のかたまり”だけ 共通化する
  • 「共通部分があるから抜き出す」は避ける
  • 小さく作るのは良いが、それは責務分離のためであり、再利用のためではない
bad
// ❌ 似ている処理を無理やり共通化した例
// modalType によって条件分岐しすぎており、拡張も変更もつらい。
// すべてのモーダルの仕様変更の影響を受ける "時限爆弾" 化しやすい。
const showModal = (modalType, options) => {
  if (modalType === 'alert') {
    console.log(`Alert: ${options.message}`);
    if (options.confirmButtonText) {
      console.log(`Confirm: ${options.confirmButtonText}`);
    }
  } else if (modalType === 'form') {
    console.log(`Form title: ${options.title}`);
    if (options.fields) {
      console.log(`Fields: ${options.fields.join(', ')}`);
    }
    if (options.onSubmit) {
      options.onSubmit();
    }
  } else if (modalType === 'image') {
    console.log(`Image path: ${options.src}`);
    if (options.caption) {
      console.log(`Caption: ${options.caption}`);
    }
  }
  // ...今後も種類が増えるたびに条件分岐が増える悪魔の増築...
};
good
//① まずは別々の関数で作る(無理な共通化をしない)
// ✔️ Alertモーダル専門の関数
const showAlertModal = ({ message, confirmText }) => {
  console.log(`Alert: ${message}`);
  if (confirmText) console.log(`Confirm: ${confirmText}`);
};

// ✔️ Formモーダル専門の関数
const showFormModal = ({ title, fields, onSubmit }) => {
  console.log(`Form: ${title}`);
  console.log(`Fields: ${fields.join(', ')}`);
  onSubmit?.();
};

//② 時間が経つと「共通していた部分」(変わらない概念)が見えてくる
//例えば、
//「モーダルは基本的に open と close ができる」
//という 変わらない概念 が見つかったとする。

//③ そこで初めて“変わらない部分”だけ共通化する
// ✔️ モーダルの「変わらない動作」だけを共通化した小さな部品
const createModalBase = () => ({
  open: () => console.log('Modal opened'),
  close: () => console.log('Modal closed'),
});

//④ 専用モーダルはこの小さな共通部品を組み合わせる
// ✔️ 特化した責務はそれぞれの関数に持たせる
const createAlertModal = ({ message }) => {
  const base = createModalBase();
  return {
    ...base,
    show: () => console.log(`Alert: ${message}`),
  };
};

const createFormModal = ({ title, fields }) => {
  const base = createModalBase();
  return {
    ...base,
    show: () => {
      console.log(`Form: ${title}`);
      console.log(`Fields: ${fields.join(', ')}`);
    },
  };
};

React Hooks

useEffectは原則使わない

参考:https://ja.react.dev/learn/you-might-not-need-an-effect

理由
・「不必要なレンダリング」「副作用のループ」「依存関係のズレが発生しやすくなる
コードが複雑になる
  • まずは使わずに実装できるか考える
  • useEffect以外の手段がない場合のみ許容する
bad
useEffect(() => {
  setFiltered(items.filter(i => i.active));
}, [items]);
good
const filtered = useMemo(() => items.filter(i => i.active), [items]);
useMemo/useCallbackはむやみに使わない
理由
Hook自体にもオーバヘッドがある
軽い処理に使うと逆効果になる場合がある
  • 必要な場合に限定して利用する
フック 使う 使わない
useMemo ・重い計算をメモ化
・依存値が変わらない限り同じ結果を使いたい値(計算結果)
・propsで「計算済みデータ」を子に渡す場合
・軽い計算(文字列結合、単純なmapなど)
・なんとなく「最適化した気」になって使う
・Reactが再レンダリングしても困らない場合
useCallback ・子コンポーネントへ「安定した関数」を渡す必要がある場合(メモ化された子など)
・再レンダリングを避けるため関数のidentity安定化が必要な場合
・再レンダリングコストが小さいコンポーネントの場合
・子コンポーネントがmemoされていない場合
単純なinline関数で十分な場合(例:onClick={()=>setOpen(true)})
useMemoを使う例
//計算コストが高い処理
const filtered = useMemo(() => heavyFilter(data), [data]);

//props 経由で子コンポーネントに渡すオブジェクトや配列を安定化したい時
const options = useMemo(() => ({ theme }), [theme]);
useCallbackを使う例
//memo 化された子コンポーネントに関数を渡すとき
const handleClick = useCallback(() => doSomething(id), [id]);
<Child onClick={handleClick} />
state化は最小限にする
理由
余計な再レンダリングが発生
バグを生みやすい
コンポーネントが複雑化する
  • 不要なstateは持たない
bad
//①:計算で求まる値を state に保存する
const [filtered, setFiltered] = useState(items.filter(...));

//②:レンダリングに不要な値を state に
const [scrollY, setScrollY] = useState(0);

//③:フォーム入力の"表示状態に関係しない"データ
const [isValid, setIsValid] = useState(false);
good
//①
const filtered = useMemo(() => items.filter(...), [items, filter]);

//②
const scrollY = useRef(0);

//③
const isValid = useMemo(() => validate(value), [value]);
  • stateの二重管理は避ける
bad
const [count, setCount] = useState(0);
const [isZero, setIsZero] = useState(true);

useEffect(() => setIsZero(count === 0), [count]); // ←二重管理
good
const isZero = count === 0;
  • サーバーコンポーネントで保持すべきデータをstate化しない
bad
//サーバーで取得した静的データをクライアントで state 化
const [items, setItems] = useState(props.items);
good
const items = props.items;
カスタムHookの分離ルール
①ビジネスロジックはカスタムhookに分離する
理由
コンポーネントをUIの描画に集中させるため
テストを書きやすくするため
コンポーネントの肥大化を防ぐため
  • API呼び出し、副作用、状態管理はカスタムフックへ分離する
  • コンポーネントは"UIのみ"を担当する
bad
const Items = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetch("/api/items")
      .then((res) => res.json())
      .then(setItems);
  }, []);

  return <ItemList items={items} />;
};
good
// useItems.ts
export function useItems() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetch("/api/items").then(r => r.json()).then(setItems);
  }, []);

  return { items };
}

// Items.tsx
const Items = () => {
  const { items } = useItems();
  return <ItemList items={items} />;
};
②カスタムHookは「1責務」だけ持つ
理由
複合ロジックを1つにまとめるとテストができない
再利用できない
修正時に破壊的変更になりやすい
  • 1Hook = 1責務になるよう意識する
bad
export function useUser() {
  // fetch
  // form
  // page
  // validation
  // pagination
}
good
export const useUserFetch = () => {};
export const useUserForm = () => {};
export const useUserPagination = () => {};
③複雑なhookはテスト可能な構造にする
理由
fetchを内部にべた書きするとテストできない
副作用をmockできない
  • 依存関係を引数で受け取る
  • テストで差し替え可能にする
bad
export function useItems() {
  useEffect(() => {
    fetch("/api/items").then(() => {});
  }, []);
}
good
export function useItems(fetcher = fetchItemsApi) {
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetcher().then(setItems);
  }, [fetcher]);

  return { items };
}

コードスタイル

三項演算子はネストしない
理由
可読性
  • 条件が複雑になる場合は if文 / 変数で事前に値を用意する
  • 「nullチェック」や「簡単な条件表示」に使う
bad
const UserStatus: React.FC<{ user?: User }> = ({ user }) => {
  // ❌ 三項演算子がネストしていて可読性が低い
  return (
    <div>
      {user
        ? user.isAdmin
          ? "管理者ユーザー"
          : "一般ユーザー"
        : "未ログイン"}
    </div>
  );
};
good
const UserStatus: React.FC<{ user?: User }> = ({ user }) => {
  let status = "未ログイン";
  if (user) {
    status = user.isAdmin ? "管理者ユーザー" : "一般ユーザー"; // ネストなし
  }

  return <div>{status}</div>;
};
三項演算子はまず??が使えるか確認する
理由
可読性
  • 冗長になるため、??で書くことを優先する
  • 値がないときのデフォルトは??が最も読みやすい
bad
const username = inputName !== null && inputName !== undefined
  ? inputName
  : "名無しさん";
good
const username = inputName ?? "名無しさん";
関数はアロー関数で定義する
理由
可読性が高い
型推論が自然
this を気にせず書ける
使い慣れてる
  • アロー関数vs関数宣言はどちらを選んでも大差ない
  • プロジェクトで統一すると可読性・保守性が向上する
引数は分割代入で受け取る
理由
型の意図が伝わりやすい
引数の順番に依存しない
props.の記述が不要になる
  • 関数コンポーネントの引数は必ず分割代入で受ける
  • 型はtypeで定義し、分割代入時に型注釈を付ける
bad
import React from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
}

// props をそのまま受けて、内部で参照している
const Button = (props: ButtonProps) => {
  return (
    <button onClick={props.onClick}>
      {props.label} {/* props.label 参照が冗長 */}
    </button>
  );
};

export default Button;
good
import React from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
}

// 引数で分割代入して、props を直接利用
const Button = ({ label, onClick }: ButtonProps) => {
  return (
    <button onClick={onClick}>
      {label} {/* 直接参照できるのでシンプル */}
    </button>
  );
};

export default Button;
省略記法を使う
理由
可読性
  • オブジェクト

    • キー名と変数名が同じ場合、省略記法を使う
    • 省略すると意味が分かりづらい場合はフル記法を許容
    bad
    const name = 'Alice';
    const age = 25;
    
    const user = {
      name: name, // 冗長
      age: age,   // 冗長
    };
    
    console.log(user);
    
    good
    const name = 'Alice';
    const age = 25;
    
    // キーと変数名が同じ場合は省略記法を使用
    const user = { name, age };
    
    console.log(user);
    
    
  • boolean型の属性

    • trueの場合は省略
    bad
    <button disabled={true}>Click</button>
    
    good
    <button disabled>Click</button>
    
  • 配列型の定義

    • 基本は[]形式
    bad
    const numbers: Array<number> = [1, 2, 3];
    
    good
    const numbers: number[] = [1, 2, 3];
    
文字列操作はテンプレート文字列を使用する
理由
可読性
  • 文字列の連結に + は使わない
  • ${}を使う
bad
// ユーザー情報を表示するサンプル
const firstName: string = "太郎";
const lastName: string = "山田";
const age: number = 25;

// テンプレートリテラルを使った文字列連結
const message = 'こんにちは、'+lastName+firstName+'さん。あなたは'+age+'歳ですね。';
const name = [firstName,lastName].join();
good
// ユーザー情報を表示するサンプル
const firstName: string = "太郎";
const lastName: string = "山田";
const age: number = 25;

// テンプレートリテラルを使った文字列連結
const message = `こんにちは、${lastName} ${firstName}さん。あなたは${age}歳ですね。`;
cosnt name = `${lastName} ${firstName}`

命名規則

ファイル
  • コンポーネント
    • PascalCase
    • 例:Button.tsx,ModalContent.tsx
  • Hook
    • camelCase + use プレフィックス
    • 例:useAuth.ts
  • utils
    • camelCase
    • 例:formatDate.ts
変数、プロパティ

テンプレート

-
理由
---


bad
bad
good
good
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?