| name | typescript-conventions |
| description | 関数型プログラミングベースのTypeScriptコーディング規約。 TypeScriptコード(.ts/.tsx)の実装時に適用する。 型、アロー関数、Result型エラーハンドリング、不変性、命名、import、async、pipe/合成を規定。 |
TypeScript 規約
基本原則
- 純関数 — 副作用なし、同じ入力 = 同じ出力
- 不変な値 — mutate せず、新しい値を導出する
- Result 型エラー — throw せず
neverthrow の Result で返す
- スコープ別厳格度 — モジュール境界では厳格に、ローカルでは柔軟に
スコープ別厳格度
| スコープ | ルール |
|---|
| export / 公開 API | Readonly<>, readonly 配列, Result 戻り値, mutation 禁止 |
| モジュール内部 | Readonly 推奨、ローカル let は封じ込めれば可 |
| 関数ローカル | 外に漏れない一時的な mutation は許容 |
型
type User = Readonly<{
id: string
name: string
roles: readonly string[]
}>
type UserInput = Omit<User, "id">
type Email = string & { readonly __brand: "Email" }
type Status = "active" | "inactive" | "pending"
const ROUTES = {
home: "/",
user: (id: string) => `/users/${id}`,
} as const satisfies Record<string, string | ((...args: never[]) => string)>
関数
アロー関数を優先。function 宣言は非推奨。
export const formatName = (user: Readonly<{ first: string; last: string }>): string =>
`${user.first} ${user.last}`
type SearchParams = Readonly<{ query: string; limit: number; offset: number }>
export const search = (params: SearchParams): ResultAsync<readonly User[], AppError> => { ... }
エラーハンドリング (neverthrow)
import { ok, err, Result, ResultAsync } from "neverthrow"
type AppError =
| Readonly<{ code: "NOT_FOUND"; id: string }>
| Readonly<{ code: "VALIDATION"; message: string }>
export const validateAge = (input: unknown): Result<number, AppError> => {
const age = Number(input)
return Number.isNaN(age) || age < 0
? err({ code: "VALIDATION", message: `Invalid age: ${input}` })
: ok(age)
}
export const createUser = (input: RawInput): ResultAsync<User, AppError> =>
validateInput(input).andThen(checkDuplicate).asyncAndThen(saveToDb)
不変性パターン
const addItem = <T>(items: readonly T[], item: T): readonly T[] => [...items, item]
const updateAt = <T>(items: readonly T[], i: number, fn: (v: T) => T): readonly T[] =>
items.map((item, idx) => idx === i ? fn(item) : item)
const buildLookup = (users: readonly User[]): ReadonlyMap<string, User> => {
const map = new Map<string, User>()
for (const u of users) map.set(u.id, u)
return map
}
非同期 (ResultAsync)
const result = await ResultAsync.combine([fetchUser(id), fetchOrders(id)])
result.map(([user, orders]) => ({ user, orders }))
const result = await validateInput(raw)
.asyncAndThen(findUser)
.andThen(checkPermission)
.asyncAndThen(execute)
Pipe / 合成
const pipe = <T>(value: T, ...fns: readonly ((v: T) => T)[]): T =>
fns.reduce((acc, fn) => fn(acc), value)
const processName = (raw: string) =>
pipe(raw, (s) => s.trim(), (s) => s.toLowerCase(), (s) => s.replace(/\s+/g, "-"))
命名
| 種類 | 規則 | 例 |
|---|
| ファイル | kebab-case | format-date.ts |
| 関数 / 変数 | camelCase | formatDate |
| 真偽値 | is/has/can 接頭辞 | isActive |
| ファクトリ | create 接頭辞 | createService |
| 定数 | UPPER_SNAKE_CASE | MAX_RETRY |
| 型 | PascalCase | UserProfile |
| イベントハンドラ | handle 接頭辞 | handleSubmit |
| コールバック props | on 接頭辞 | onSubmit |
Import 順序
external → @/ エイリアス → 相対パス → import type
import { z } from "zod"
import { ok, type Result } from "neverthrow"
import { parseWith } from "@/lib/zod-utils"
import { validate } from "./validate"
import type { User } from "./types"
禁止事項
function 宣言 → アロー関数を使う
class → plain object + functions
enum → ユニオン型
any → unknown + 型の絞り込み
- データ型の
interface → type + Readonly
- 期待されるエラーの
throw → Result
export default → named export
- ミュータブルなグローバル状態