関数型プログラミングベースのTypeScriptコーディング規約。 TypeScriptコード(.ts/.tsx)の実装時に適用する。 型、アロー関数、Result型エラーハンドリング、不変性、命名、import、async、pipe/合成を規定。
neverthrow の Result で返す| スコープ | ルール |
|---|---|
| export / 公開 API | Readonly<>, readonly 配列, Result 戻り値, mutation 禁止 |
| モジュール内部 | Readonly 推奨、ローカル let は封じ込めれば可 |
| 関数ローカル | 外に漏れない一時的な mutation は許容 |
// interface ではなく type alias + Readonly
type User = Readonly<{
id: string
name: string
roles: readonly string[]
}>
// 型の導出
type UserInput = Omit<User, "id">
// ドメインプリミティブにはブランド型
type Email = string & { readonly __brand: "Email" }
// enum ではなくユニオン型
type Status = "active" | "inactive" | "pending"
// 設定オブジェクトには as const satisfies
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}`
// 引数3つ以上 → パラメータオブジェクト
type SearchParams = Readonly<{ query: string; limit: number; offset: number }>
export const search = (params: SearchParams): ResultAsync<readonly User[], AppError> => { ... }
import { ok, err, Result, ResultAsync } from "neverthrow"
// ドメインエラーは判別可能ユニオンで定義
type AppError =
| Readonly<{ code: "NOT_FOUND"; id: string }>
| Readonly<{ code: "VALIDATION"; message: string }>
// throw せず Result で返す
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)
}
// andThen / map でチェーン
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)
// ローカルスコープの mutation は封じ込めれば OK
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
}
// 並行
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)
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 |
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 + functionsenum → ユニオン型any → unknown + 型の絞り込みinterface → type + Readonlythrow → Resultexport default → named export