Upgrade to Pro — share decks privately, control downloads, hide ads and more …

共通化で考えるべきは、実装より公開する型だった

 共通化で考えるべきは、実装より公開する型だった

TSKaigi 2026 After Eventである「TSKaigi 2026事後勉強会」での発表資料です。
https://smarthr.connpass.com/event/392342/

【当日のスライドからの加筆について】
登壇時のスライドは口頭での補足を前提とした構成だったため、スライドだけでは
意図が伝わりにくい箇所がありました。そこで本資料では、

・スライド単体で読んでも内容を追えるよう、アンダーライン付きのテキストで補足を追加
・発表では時間の都合で触れなかった関連トピック(Card コンポーネント/zod の
 共通化など、同じ観点で考えられる具体例)のスライドを追加

しています。発表を聞いていない方でも読み進められる内容になっています。

【参考文献】
・Robert C. Martin著『Clean Architecture 達人に学ぶソフトウェアの構造と設計』https://www.kadokawa.co.jp/product/301806000678/
・Tom Long著『Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考 』https://www.shuwasystem.co.jp/book/9784798068169.html

Avatar for code-egg

code-egg

June 27, 2026

Other Decks in Programming

Transcript

  1. 数え⽅ 戻り値と引数の項⽬ひとつずつに、こ う聞く 「これは、誰の都合で変わる?」 ひとつ → 共通化しても大丈夫 2つ以上 → 共通化しすぎのサイン

    4 私は単一責任原則を「変更理由が一つのグループからくる ように分けよう。」と理解をした。 アクターが増える=各所からコードの変更依頼が来て変更 が大変になる。全員の都合を叶えるためにIF文まみれに なったりいろんな箇所を変更しないといけないかもしれ ない。 ここで1つという数の話になった。では、どうやって アクターの数を数えればよいのでしょうか? ここでさっきの型の話になります。 引数と戻り値の型に左記の質問をします。 誰にあたるのがアクターで、もし2つのアクターから別の 理由で要求が来るなら、共通化に失敗する可能性があ る。
  2. 失敗例:型が漏れたラッパー function fetcher<T>( config: AxiosRequestConfig ): Promise<AxiosResponse<T>> { return axios.request<T>(config)

    } config は誰の都合で変わる? → axiosの都合 関数で隠しても、型が漏れていれば 依存は隠せていない 5
  3. 境界を作る = 独⾃型に置き換える type RequestConfig = { url: string; method?:

    "GET" | "POST" } type Result<T> = | { ok: true; data: T } | { ok: false; error: ApiError } export async function fetcher<T>( config: RequestConfig ): Promise<Result<T>> { /* 中⾝の実装は自由 */ } 公開型を独⾃にする = ライブラリの 都合を締め出し、境界を作ること 6
  4. 実体験:message を共通化したら壊れた type Result<T> = | { ok: true; data:

    T } | { ok: false; message: string } message は誰の都合で変わる? → デザイナー・ユーザー アクターが2つ。これが SRP 違反の正体 余計な型を載せる = 他アクターの 変更理由を、⾃分の中に抱え込むこと 7 例えばconst msg = toMessage(errCode)みたいなコード これを共通化したfetch関数の中でやると危険
  5. どう直したか type Result<T> = | { ok: true; data: T

    } | { ok: false; code: ErrorCode } fetcher は message を知らなくていい 表⽰は呼び出し側の責務 他アクターの関⼼は、結果から 派⽣させて呼び出し側へ 8 共通化関数であるfetcher関数はerrorCodeを返して 呼び出し側でメッセージを決める。 const result = fetcher(param) const msg = toMessage(result.code)
  6. おまけ:共通化の難易度マップ 副作⽤と変換が増えるほど、型の設計が効く L1 プリミティブ・配列 → 最も共通化しやすい L2 特定の型に依存したデータ L3 L2

    + 外部I/O(fetch → ) axiosラッパー L4 L3 + 変換 → 今⽇の message の話 L5 L4 を⼊れ⼦で呼ぶ → DIを考え始める 副作⽤と変換が増えるほど、 「公開する型」の設計が効いてくる 11
  7. Cardはすぐ太る <Card title=" ユーザー情報" description=" 登録済みユーザーです" actions={<Button> 編集</Button>} variant="warning" showBorder

    dense clickable badge="NEW" footer={<UpdatedAt />} /> Cardが「画⾯ごとの差分」を吸い込み始める 3
  8. Cardの公開型を数える props 誰の都合で変わる? title 画⾯の情報設計 description 画⾯の⽂⾔ actions ユースケース variant

    デザイン clickable 画⾯遷移 badge 業務状態 dense レイアウト propsに複数のアクターが現れている 4
  9. Cardは責務を⼩さくする <Card> <CardHeader> <CardTitle> ユーザー情報</CardTitle> <CardDescription> 登録済みユーザーです</CardDescription> </CardHeader> <CardContent> <UserSummary

    /> </CardContent> <CardFooter> <Button> 編集</Button> </CardFooter> </Card> Cardは構造だけを担当し、中⾝の意味は呼び出し側に残す 5
  10. zodが漏れている状態 export const userSchema = z.object({ name: z.string().min(1), email: z.string().email(),

    }); export type UserInput = z.infer<typeof userSchema>; 呼び出し側が zod を知っている 8 z.objectなどはschema.tsのようなファイルの閉じ込め て、zodから生成した型定義だけexportするようにする。 schema関してはstandard schemaがあるので、許容でき る可能性がある。 例えば、React Hook Formではstandard schema用の resolverがある。
  11. zodを境界の内側に置く type ValidationResult<T> = | { ok: true; data: T

    } | { ok: false; issues: ValidationIssue[] }; type ValidationIssue = { path: string; code: string }; function validateUserInput(input: unknown): ValidationResult<UserInput> { const result = userSchema.safeParse(input); if (result.success) return { ok: true, data: result.data }; return { ok: false, issues: result.error.issues.map((i) => ({ path: i.path.join("."), code: i.code, })), }; } 外に⾒せるのは zod の型ではなく、⾃分たちの結果型 9 userSchema.safeParseを直接使用してしまうと、 zod依存になるため、関数に閉じ込め境界を作る
  12. 具体例まとめ 対象 漏れやすいもの 境界の作り⽅ axios AxiosRequestConfig , AxiosResponse 独⾃の RequestConfig

    , Result Card 画⾯固有props 構造だけを共通化 zod ZodError , safeParse 結果 独⾃の ValidationResult message 画⾯⽂⾔ code だけ返して画⾯で変換 13