メインコンテンツまでスキップ

第7章:TypeScriptの地雷②:exportの変更と“見えてる面”📤👀🍌✨

できるようになること🎯

  • 「どこまでが“公開API(見えてる面)”か」を自分で線引きできる✍️
  • exportの変更が なぜほぼ破壊的変更(MAJOR) になりやすいか説明できる💥
  • index.ts を“門番”にして、事故りにくい公開面を設計できる🧩✨
  • package.jsonexports も「公開面の一部」って理解できる📦👀 (Node.js)

7.1 「見えてる面(API surface)」ってなに?🌊👀

API surface = 利用者(他人のコード)が触れる“入口”ぜんぶ だよ〜📣✨ TypeScriptだと、特にこのへんが入口になりがち👇

  • export されてる関数・クラス・定数・型(type / interface)🧰
  • index.ts(バレル)から見えるもの(= だいたい公開API扱い)🚪
  • npmパッケージとしての入口(package.jsonexports / main など)📦
  • (型配布してるなら).d.ts に出てくる宣言たち🧷

ポイントはこれ👇 「export した瞬間、それは“約束”になりやすい」🤝💖 そして約束を変えると、利用者が泣く😭


7.2 export変更が“ほぼ破壊”になる3大理由💥💥💥

理由①:importが物理的に壊れる😇(名前・場所・形が変わる)

たとえば利用者がこう書いてたら…

import { parseUser } from "your-lib";

あなたが parseUser をリネームしたり削除したら…

  • コンパイル通らない(= 即死)💀
  • だから基本 MAJOR になりやすい💥

exportの削除・リネームは典型的な breaking change だよ〜 (Semver TS)


理由②:「型だけ export」と「値も export」は別物🧠🧷⚡

TypeScriptには type-only export があるよね👇 export type型としては使えるけど、実行時には消える(出力JSに残らない)✨ (TypeScript)

// ✅ 型だけ export(実行時には存在しない)
export type UserId = string;

で、ここが地雷💣 もともと “値” として存在してた export を、型だけに変える と利用者の実行時コードが壊れることがある😱 (例:export classexport type 的な扱いに寄せる、など)

✅ 「値の export を消す」のは breaking change になり得るよ〜 (Semver TS)


理由③:package.jsonexports は「公開面をロックする」🔒📦

Exports Lock

exports を使うと、“ここから先は入っちゃダメ🙅‍♀️” を作れるの。 Node.jsの公式ドキュメントでも、exportsエントリーポイントを環境ごとに切り替えられるimport/require等)って説明されてるよ📌 (Node.js)

つまり…

  • exports に載ってないパスは、利用者から 見えない/使えない 扱いになりやすい👀
  • 後から exports を導入すると、深いパス import(例:your-lib/dist/foo が急に死ぬことがある💥
  • exports の変更は、入口の変更=公開面の変更だよね? → 破壊になりやすい😇

7.3 “門番”パターン:index.ts で公開面をコントロールしよ🧩🚪✨

✅ おすすめ方針:公開は「1か所からだけ」📣

フォルダ構成イメージ👇

src/
index.ts ← 公開APIの門番👮‍♀️✨
internal/
heavyLogic.ts ← 内部(直接importさせない)
features/
user.ts

src/index.ts はこんな感じ👇

// ✅ 公開していいものだけ“明示的に”export
export { createUser } from "./features/user";
export type { User, UserId } from "./features/user";

// ❌ なんとなく export * は事故りやすい(公開面が勝手に増える😇)

なぜ “明示的 export” がいいの?🤔

  • 公開面が増減した瞬間に気づける👀
  • 「これは公開の約束です」って自分で自覚できる🤝
  • 誤爆で内部型/内部関数を外に出しにくい🙅‍♀️

7.4 ありがち事故パターン集😵‍💫(全部“SemVer判断”つき)

事故①:export名変更(=だいたいMAJOR)💥

  • export { foo }export { bar } に変える → 利用者のimportが壊れる😭 → MAJOR

優しいやり方🫶

  • bar を追加(MINOR)✨
  • foo は残しつつ @deprecated(次章の非推奨につながる🪜)

事故②:default export ↔ named export の入れ替え(MAJOR)💣

// 変更前
export default function hello() {}

// 変更後
export function hello() {}

利用者が

import hello from "your-lib";

してたら崩壊😇 → MAJOR


事故③:exportされた型の“ちょい変更”が大事故🧷💥

「型は実行時に消えるし、軽く変えてOKでしょ?」って思いがちだけど… 利用者のコンパイルが落ちるなら、普通に破壊だよ〜😭

(型の地雷は前章でやった通り⚠️)


7.5 公開面を“漏らさない”テク:@internal + stripInternal 🕵️‍♀️🧹

「内部のつもりだったのに .d.ts に出ちゃった〜😭」って時に役立つのがこれ👇

  • /** @internal */ を付ける
  • tsconfigstripInternal: true@internal の宣言を .d.ts から落とせる (TypeScript)

(ただし公式にも “内部向けオプションだよ” 的な注意があるので、使うならチームでルール化が安心🫶) (TypeScript)


7.6 SemVerに落とす:この章の判断チート表🎯✨

  • MAJOR:exportの削除 / リネーム / default↔named入替 / exports の入口変更 📤💥
  • MINOR:新しいexportを追加(既存を壊さない範囲)➕✨
  • PATCH:内部実装だけ変更(公開面が完全に同じ)🧹🐛

迷ったらこの順でチェック✅

  1. 公開面(export/exports)に触った?
  2. 利用者のimportが壊れる?
  3. 型チェックや挙動の意味が変わる? → YES が出たら だいたいMAJOR寄り😇

ミニ演習🎓✨:index.ts 公開面を設計してみよう🧩

お題:このモジュール、どれを公開する?👀

// src/features/user.ts
export type UserId = string;

export interface User {
id: UserId;
name: string;
}

export function createUser(name: string): User {
return { id: crypto.randomUUID(), name };
}

// src/internal/debug.ts
export function dumpUser(u: User) {
console.log(u);
}

やること✍️

  1. src/index.ts を作って「公開するものだけ」exportしてね👮‍♀️✨

  2. dumpUser は公開すべき?しないべき?理由も書いてね📝

  3. 次の変更はSemVerでどれ?🎯

    • A: createUsermakeUser に改名
    • B: Userage?: number を追加
    • C: dumpUserindex.ts に追加で公開

AI活用🤖✨(Copilot / Codexに投げる“そのままプロンプト”集)

① 公開面の棚卸し👀

このTypeScriptライブラリの「公開API surface」を列挙して。
index.ts の export 一覧と、package.json の exports で公開されている入口も含めて。
破壊的変更になりうるポイントも指摘して。

② 変更のSemVer判定🎯

次の変更はSemVerで MAJOR/MINOR/PATCH のどれ?理由も初心者向けに説明して。
(変更内容をここに貼る)

③ “優しい壊し方”提案🫶

この破壊的変更を、非推奨→移行→削除の3段階で進めるプランを作って。
利用者向けの移行ガイド(短め)も書いて。

まとめ🍌✨(この章のコアだけ!)

  • export = 約束🤝 → 変えると壊れやすい💥
  • index.ts を門番にして、公開面を小さく保つ👮‍♀️🚪
  • package.json exports も公開面の一部📦👀 (Node.js)
  • type-only export は実行時に消えるので、値との入替は特に注意⚡ (TypeScript)
  • 内部を漏らしたくない時は @internal + stripInternal が使える🧹 (TypeScript)

次の章(第8章)は、この公開面を前提に 「互換ポリシーを6行で書く📜✨」 ところに入っていくよ〜!続けて作る?😆💖