Building TypeScript

0
TypeScript を「他の一般的な言語」と比べた 12 の特徴と観点
# | TypeScript ならではの観点 | ざっくり要点 | 他言語との主な違い / 似ている概念 |
---|---|---|---|
1 | 構造的型付け(Structural Typing) | “形が同じなら同じ型” 名前や継承階層ではなくプロパティ構造で互換判定 |
Go も近いが、Java/C# は名義(Nominal)型付け |
2 | 型注釈(Type Annotation) |
: number や as const で 値に型ラベルを明示。推論より優先し、リファクタ時の意図の保存と API 境界のドキュメント化に有効 |
Kotlin/Swift は必須箇所が多いが TS は “必要最小限” で書ける |
3 | ジェネリクス(Generics) |
function identity<T>(v:T):T のように 型をパラメータ化。共通ロジックを 型安全に再利用しつつ、制約 (<T extends U> ) で範囲も限定可 |
Java/C# は JVM/CLR が型情報を部分保持、TS は完全消去 |
4 | 消える型(型消去) | トランスパイル後は型情報が完全に消滅 → ランタイムには存在しない | Java/C# はバイトコードに一部残存(リフレクション可) |
5 | ユニオン / インターセクション型 |
A|B … 排他的バリアント を表現し、タグ付きユニオンで状態遷移を型安全に判定。A & B … 両立(Mixin) を合成して既存 API を拡張。→ ADT 的に扱え、never 網羅性チェックも機能。 |
Swift/Kotlin の sealed class(ユニオン)/多重継承禁止の代替(インターセクション) |
6 | リテラル & テンプレートリテラル型 |
リテラルユニオン "GET" | "POST" で 許可値を列挙、テンプレートリテラル型 `${Prefix}/${Id}` で 文字列を型安全生成。→ フリーテキストの typo を ビルド時に排除、実行時コストは 0。 |
Rust の列挙型 + マクロ/C++ の constexpr 文字列に近いが、より動的に型生成可能 |
7 | 条件型 & 型推論ツール |
T extends U ? X : Y , infer キーワードでメタ型演算 |
C++ のテンプレートメタプログラミング的だが安全・簡潔 |
8 | マップド型 / Utility Types |
Partial<T> , Record<K,T> , keyof , in で型を変形 |
Haskell の type families にニュアンスが近い |
9 | strict null checks & 逐次強化 |
strict: true で “潜在的に undefined” を厳密追跡徐々に any をなくす足場が豊富 |
Kotlin の ? や Swift の Optional はランタイムでも区別 |
10 | JS 互換と段階的導入 |
既存 JS 100 % 有効 + 拡張子 .ts に変えるだけで移行開始 |
他トランスパイル言語(Elm, ReasonML)はフルリライト |
11 | 宣言ファイル / 型定義の分離 |
*.d.ts , DefinitelyTyped, モジュール拡張(Module Augmentation) |
C/C++ の header に近いが、merge ルールが独特 |
12 | 型安全と DX の相関 | IDE 補完・リネームリファクタ・バグ早期検知が劇的向上 → “型を書くのではなく得る” 体験 | JetBrains 系 IDE + JVM 言語の感覚に似つつ、ブラウザ即確認の速さ |
インサイト & 立ち止まるポイント
- “型はコードの圧縮表現” – ロジックを型に持ち上げると実装コードがシンプル化しテスト負荷も減る。
-
ランタイム検証ギャップ – 型消去ゆえに
zod
,io-ts
など 実行時スキーマ が要るケースを意識。 -
パフォーマンス vs 型安全 – 型演算はビルド時コストになる。巨大プロジェクトでは
skipLibCheck
,isolatedModules
のトレードオフを検討。 -
“赤波線ゼロ” を保つ文化 – ESLint
no-implicit-any
,@typescript-eslint/consistent-type-imports
等で 型逸脱を早期ブロック。 -
ユーティリティ型の自作 – プロジェクト固有ドメインを
type ID = Branded<string, "ID">
のように“単位付き”化するとバグ激減。
具体的アプローチ 3 通り
アプローチ | どう進めるか | 向いているケース |
---|---|---|
① TDD ↔ Type-Driven Design | “テスト前に型で仕様を表す”。tsd で型の単体テストを書く |
API/ライブラリ設計 |
② JS → TS 漸進移行 |
// @ts-check + JSDoc 型記法 → .ts 化 → strict 強化 |
既存 JS 資産が多い |
③ 型レベルプログラミング学習 | Playground で条件型パズルを解く。“型で Fibonacci” など | アルゴリズム好き・型厨 |
まとめ
TypeScript の真価は 「JS の柔軟さ × 静的型の安心感」 を“後付け”できる点にあります。
構造的型付け・型注釈・ジェネリクスなど豊富な型機能を活かし、「まず動かす」から 「型で壊れない設計」 へ一段上げるのが到達目標です。

1
1️⃣ まず押さえるべき “構造的型付け” の定義
観点 | 内容 |
---|---|
判断基準 | “その値が 必須プロパティを 全部持っているか” だけを見る ── 名前・継承階層は無関係 |
結果 | 静的解析 上で “型 A ≒ 型 B” とみなされる範囲が広い(=柔軟) |
主なメリット | - 既存 JS オブジェクトをほぼ無改変で型付けできる - Duck Typing の安全版として API / テストが書きやすい |
主な落とし穴 | - “たまたま形が同じ” ものまで代入可 → 意図しない互換 が混入する恐れ - ドメイン固有 ID など 区別すべき同型 を区別できない |
2️⃣ 具体例で理解する構造的互換
✨ 基本ルール:“必要な要素をすべて満たせば OK”
interface Person {
name: string;
age: number;
}
const alice = { name: "Alice", age: 30, hobby: "golf" }; // 余分なプロパティあり
const bob: Person = alice; // ✅ OK ― hobby は無視される
ポイント
-
alice
は 最低限name
とage
を持つのでPerson
と互換。 - 追加プロパティは黙認 — これが “幅優先” の互換判定。
✨ 関数型にも適用(引数 “少ないほうが安全” のワケ)
type Greeter = (name: string) => void;
const greetExtra = (name: string, time: Date) => {
console.log(`Hello ${name} @ ${time.toLocaleTimeString()}`);
};
let fn: Greeter = greetExtra; // ✅ OK
// 呼び出し側は 1 引数で渡すので問題なし
関数パラメータは「受け取る側が 少ない ほど安全」 という協変/反変ルールを
構造的 に検証している点がポイント。
✨ 逆に “過不足” があるとエラー
const charlie = { name: "Charlie" }; // age が足りない
const p2: Person = charlie; // ❌ 型エラー
3️⃣ “名義型が欲しい” 場面と Brand(擬似 Nominal)パターン
type UserID = string & { __brand: "UserID" };
type PostID = string & { __brand: "PostID" };
function getPost(id: PostID) { /* ... */ }
const raw = "abc123" as UserID;
getPost(raw); // ❌ 期待どおり弾かれる
-
string & { __brand: ... }
で “見た目は同じ文字列でも別物” を表現。 -
構造的ルールを逆手 に取り、わざと 存在しない幽霊プロパティ を重ねることで
Nominal 風の厳格さを得る。
4️⃣ React コンポーネントでの実践例
type ButtonBase = {
label: string;
onClick: () => void;
};
type PrimaryButton = ButtonBase & { variant: "primary" };
type SecondaryButton = ButtonBase & { variant: "secondary" };
type ButtonProps = PrimaryButton | SecondaryButton;
export const Button: React.FC<ButtonProps> = (props) => {
return (
<button
style={{ background: props.variant === "primary" ? "#1976d2" : "#808080" }}
onClick={props.onClick}
>
{props.label}
</button>
);
};
-
PrimaryButton
とSecondaryButton
は 構造的に共通部分 を合成。 - さらに
variant
リテラルで Union による“分岐可能な同型” を表現。 - 呼び出し側は
variant
値で 型が絞り込まれ props が安全に扱える。
5️⃣ ありがちな落とし穴と防御策
落とし穴 | 例 | 防御策 |
---|---|---|
“たまたま同じ形” の誤注入 |
{ id: 1 } を User / Order 両方に代入できる |
Brand 型 or @typescript-eslint/consistent-type-definitions
|
空オブジェクト {} が何でも通る | function takesConfig(c: {} ) |
具体的型 or ジェネリック制約 <T extends Record<string, unknown>>
|
Enum のゆるさ |
enum Role { Admin, Guest } は number と互換 |
const enum + リテラル型 or as const オブジェクト |
6️⃣ “型安全 × 柔軟” を両立させる設計指針
-
“外部境界”では Brand / Discriminated Union で厳密化
- API 入力・DB ID など 違うけど形が同じ ものは区別する。
-
“内部流通”は構造的利便性を活かし最小限のフィールドで伝搬
- DTO → ViewModel と段階的に 型を絞る とテスト容易。
-
exactOptionalPropertyTypes
をtrue
に(TS 4.4+)- “あるかも・無いかも” をより厳しく判定でき、思わぬ互換を防ぐ。
-
型テスト (
tsd
,expectTypeOf
) を CI に組み込む- コンパイル通過だけでなく 想定した互換 を自動チェック。
7️⃣ すぐ試せるミニチャレンジ 🏃♂️
-
ブランド型で UserID ↔ Email を区別しつつ
map<UserID, User>
のキーに使ってみる。 -
関数互換クイズ – 以下が assignable かどうかを予想 → tsc で検証
type F1 = (a: number, b: number) => void; type F2 = (a: number) => void; let f: F1 = F2 ??? // OK or Error?
-
Playground で “余計なプロパティを落とす Utility 型” を自作
type Exact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? T : never : never;
🚀 まとめ
- 構造的型付け = “プロパティで見る互換判定”。柔軟だが 異なる概念を混同 しやすい。
- ブランド型・Discriminated Union で 必要な所だけ 名義型風に締めると実務品質◎。
- React/Next.js でも Props, API DTO, 状態遷移 を “形+タグ” で設計すると
テスト工数とバグ混入が激減。

2
0️⃣ “型注釈” をひとことで
観点 | 要点 |
---|---|
定義 |
値・変数・関数に “この型であるべし” とラベルを貼る 文法 (: Type ) |
役割 | ① 型推論の不足を補う ② 意図をドキュメント化 ③ リファクタ耐性を上げる |
書き所の原則 | “境界” と “あいまいな推論” のみ。内部実装は 推論に任せて可読性↑ |
1️⃣ 代表的な注釈ポイントとサンプル
1-1. 変数宣言
let count: number = 0; // 明示で可読性↑
const title = "hello"; // 推論で string
1-2. 関数シグネチャ
function add(a: number, b: number): number {
return a + b;
}
- 入出力をドキュメント化 + コンパイラが 実装漏れ を検知。
1-3. オブジェクトリテラル “as const”
const config = {
apiBase: "/v1",
retry: 3,
} as const; // → readonly & リテラル型
-
"v1"
や3
が リテラル型 へ昇格 → 誤代入を防止。
as
)
1-4. 型アサーション (const el = document.getElementById("root") as HTMLDivElement;
// “null じゃない” ことを自分で保証
- 安全かどうかは自分の責任。なるべく Type Guard で代替。
2️⃣ “境界で書く” ― 実務フロー別ベストプラクティス
境界 | 例 | 目的 | 注釈 or 推論 |
---|---|---|---|
API 契約 | fetch した JSON を z.infer<typeof User> に |
外部データを 型安全化 | 注釈必須 |
Public API | ライブラリの公開関数 | 利用者への 自己文書化 | 注釈必須 |
内部実装 | 算術・フロー制御 | コンパイラが推論可 | 推論に委ねる |
Widening 防止 | const dirs = ["up","down"] |
リテラル → string に広がるのを阻止 |
as const |
3️⃣ React / Next.js での型注釈
3-1. コンポーネント Props
type ButtonProps = {
label: string;
onClick: () => void;
};
export const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
- 外部公開面 は注釈で明示 → Storybook / IDE でも自動ドキュメント化。
3-2. 状態 Hook と “初期値 any” 罠
const [data, setData] = useState<string | null>(null); // ✅
// const [data] = useState(null); // ❌ data: any になる
-
初期値が
null
/[]
だけだと推論が弱い → 注釈必須。
useRef
の Non-null 記法
3-3. const inputRef = useRef<HTMLInputElement>(null!); // null! = 宣言的に非 null
4️⃣ 型アサーション vs 型キャスト ― 混同しない使い分け
シンタックス | 何をするか | いつ使うか | 危険度 |
---|---|---|---|
as Type |
“これは Type とみなせ” と 言い切る | DOM 要素取得、外部ライブラリ | ⚠️ 高 |
<Type>{…} 型引数の明示 |
ジェネリクス推論を補助 | useState<T[]>([]) |
低 |
原則:情報が外から来る 場面は 必ずランタイム検証 (
zod.parse
) を挟む。
5️⃣ よくある 落とし穴 とガード策
落とし穴 | 例 | 対策 |
---|---|---|
暗黙の any | function fn(x){…} |
noImplicitAny: true |
リテラル幅拡張 |
const dir = "up" → string
|
as const / リテラルユニオン型 |
冗長注釈 |
let i: number = 0 (推論で充分) |
ESLint @typescript-eslint/no-inferrable-types
|
過剰キャスト | value as unknown as User |
Type Guard を書く |
6️⃣ すぐ試せるミニチャレンジ 🏃♀️
-
“引数が増えたらエラー” テスト
function greet(person: {name:string}) { … } // 呼び出し側で name typo を起こしてみる
-
Readonly<T>
とas const
の違い をコードで比較。 -
初期値が空配列の
useState
で推論を観察し、型引数の有無で IDE 補完がどう変わるか確認。
🚀 まとめ
- 型注釈 = コンパイラと未来の自分へのメッセージ。
- 境界にだけ書く/内部は推論に任せる が “読みやすさ×安全” の黄金比。
- React/Next.js では Props・Hook 初期値・Ref が 3 大注釈ポイント。
👉 自分のプロジェクトで「暗黙 any」「広がりすぎ string」 を検索し、
必要十分な注釈を追加して “赤波線ゼロ & すっきりコード” を体感してみてください!

3
0️⃣ “ジェネリクス” を 30 秒で掴む
観点 | 要点 |
---|---|
定義 | “型を引数として受け取る仕組み”。値の再利用を 型安全 にスケールさせる。 |
コア構文 |
<T> , <T extends U> , <T = Default> , <T extends keyof O>
|
効果 | 共通ロジックの 抽象度↑・重複↓、リネーム時も型が追随=保守コスト↓ |
1️⃣ 基本形:“そのまま返す” アイデンティティ関数
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T = number
const text = identity("hello"); // T = string
-
T
は呼び出し時に推論。明示する場合はidentity<string>("hi")
。 - 戻り値も引数も同一 T なので 型安全に再利用できる。
<T extends …>
2️⃣ 制約付きジェネリクス:✨ プロパティ存在を保証して安全にアクセス
function lengthOf<T extends { length: number }>(v: T) {
return v.length;
}
lengthOf([1, 2, 3]); // OK (length プロパティあり)
lengthOf("abc"); // OK
lengthOf(123); // ❌ number に length なし
-
extends
で 許容範囲 を指定。 - JS ランタイムエラーを ビルド時に潰す 典型例。
3️⃣ キー制約:インデックス操作を型安全に
function getProp<
O,
K extends keyof O
>(obj: O, key: K): O[K] {
return obj[key];
}
const user = { id: 1, name: "Alice" };
getProp(user, "id"); // number
getProp(user, "age"); // ❌ "age" は存在しない
-
K extends keyof O
で “存在するキーのみ” を受け付け。 - 戻り値の型が O[K] → 取ったキーに応じ自動で変化 するのが強み。
4️⃣ デフォルト型 & 再帰:ユーティリティの柔軟性アップ
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Settings {
theme: { color: string; fontSize: number };
autoSave: boolean;
}
const patch: DeepPartial<Settings> = {
theme: { color: "blue" } // fontSize 省略 OK
};
- 再帰的ジェネリクス で “任意深さの Partial” を実現。
-
?<T = {}>
で既定型を用意すれば呼び出し側の負担を減らせる。
5️⃣ React × MUI での実践:汎用 Table コンポーネント
type Column<T> = {
header: string;
accessor: (row: T) => React.ReactNode;
};
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
}
export function DataTable<T>({ data, columns }: DataTableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map(c => <th key={c.header}>{c.header}</th>)}</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(c => <td key={c.header}>{c.accessor(row)}</td>)}
</tr>
))}
</tbody>
</table>
);
}
<DataTable
data={[{ id: 1, name: "Alice" }]}
columns={[
{ header: "ID", accessor: r => r.id },
{ header: "Name", accessor: r => r.name },
]}
/>
-
コンポーネント全体を
T
で抽象化。 - 列定義側で
accessor
が row の型を継承し、typo を即検知。
6️⃣ 高階関数・カリー化 とジェネリクス
const withRetry =
<TArgs extends any[], TResult>(fn: (...a: TArgs) => Promise<TResult>) =>
async (...args: TArgs): Promise<TResult> => {
for (let i = 0; ; i++) {
try { return await fn(...args); }
catch (e) { if (i >= 2) throw e; }
}
};
const fetchJSON = async (url: string) => (await fetch(url)).json();
const fetchWithRetry = withRetry(fetchJSON); // 型が完全維持
- 渡した関数のシグネチャをそのまま保持 → ラッパーを挿んでも DX 劣化なし。
7️⃣ よくある 落とし穴 と防御策
落とし穴 | 例 | 防御策 |
---|---|---|
ジェネリック漂流 (any ) |
function wrap<T>(v){ return v } |
必ず引数 or 戻り値に T を置く |
過剰な <T> 乱用 |
function log<T>(msg:string){…} |
推論で済む所はジェネリクス不要 |
型消去ゆえの typeof T |
if (typeof T === ...) |
ランタイム分岐は不可→別パラメータで渡す |
循環制約 | <T extends U, U extends T> |
設計見直し。多くは keyof ミス |
8️⃣ すぐ試せるミニチャレンジ 🔧
-
“配列をフラット化する型” を実装
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
-
pick<T, K extends keyof T>
を手書きで作り、Pick
と差を比べる。 -
React.ComponentType<P>
相当を実装し、HOC の理解を深める。
🚀 まとめ
- ジェネリクスは “型の関数化”。再利用性 × 安全性 を同時に高める鍵。
-
制約 (
extends
)・キー制約 (keyof
)・デフォルト型 を組み合わせ、抽象度と実用性を両立。 - React / Next.js でも 汎用 UI・HOC・API ラッパー など至る所で威力を発揮。

4
0️⃣ “型消去” とは何か ― 30 秒まとめ
観点 | TypeScript の挙動 |
---|---|
定義 |
コンパイル(tsc )後、型情報が JavaScript に一切残らない 現象 |
目的 | TS は 完全互換 の JS を出力するトランスパイラ ⇒ ブラウザ / Node が型を理解できない前提 |
結果 | - 型安全はビルド時 に出し切る必要 - ランタイム検証が欲しい場合は 別途スキーマ が要る |
1️⃣ 目で見る“消える型” ↔ 他言語との比較
🔍 例:ジェネリック & インターフェース
// echo.ts
export function echo<T>(value: T): T {
return value;
}
interface User { id: number; name: string }
const u = echo<User>({ id: 1, name: "Alice" });
$ tsc --target ES2020 --module esnext
// echo.js (抜粋:型が全滅🎩🪄)
export function echo(value) {
return value;
}
const u = echo({ id: 1, name: "Alice" });
言語 | ジェネリックの実体 | ランタイムで型参照可? |
---|---|---|
TypeScript | 完全削除 | ❌ |
Java (JVM) | 型擦り切れ(Raw Type)が残るが <T> 情報は消える | 部分的に可(リフレクション) |
C# (.NET) | ほぼ完全保持 | ✅ |
Rust / C++ | Monomorphization(具象型ごとに実体生成) | ✅ (バイナリ単位) |
2️⃣ “消える”ことが生む 3 つの落とし穴
落とし穴 | 具体例 | 典型バグ |
---|---|---|
① 型を期待したランタイム分岐 | if (typeof T === "string") … |
常に undefined で死ぬ |
② API 受信後の思い込み |
const data: User = await fetch() (実際は欠損プロパティ) |
画面クラッシュ or 404 |
③ フロント・バック間のズレ | サーバー側は JSON スキーマ変更、TS 側は通る | “動くけど壊れる” 悪質バグ |
3️⃣ 実務で使う 3 つの防御パターン
3-1. ユーザー定義 Type Guard
type User = { id: number; name: string };
function isUser(x: unknown): x is User {
return typeof x === "object"
&& x !== null
&& "id" in x
&& typeof (x as any).id === "number"
&& "name" in x
&& typeof (x as any).name === "string";
}
const json = JSON.parse(payload);
if (!isUser(json)) throw new Error("invalid user"); // runtime guard
zod
, @sinclair/typebox
, yup
…)
3-2. スキーマ → 型自動生成(import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>; // ← 型を“得る”
const user = UserSchema.parse(JSON.parse(payload)); // runtime + compile OK
satisfies
)
3-3. コンパイル時のみ型検証するユーティリティ (const config = {
apiBase: "/v1",
retry: 3,
} as const satisfies Record<string, string | number>; // TS 4.9+
ランタイム負荷ゼロ で「構造チェックだけ欲しい」ケースに便利。
4️⃣ “型消去” × React / Next.js で遭遇する典型シーン
シーン | 問題 | 解決策 |
---|---|---|
API Route ↔ フロント | DTO 変更が silent break | 共通 zod スキーマ / openapi-typescript で 1 ソース化 |
getServerSideProps の戻り値 | 戻り値型 ≠ 実データ |
return z.infer<typeof PageData> パターンで整合 |
Form 送信 |
event.target.value は string 固定 |
型レベルで絞り込む → runtime で parse (Number() , Date ) |
5️⃣ ジェネリックの“消え方”を突き詰める 3 ステップ
-
Playground で
T
をconsole.log
してみるfunction foo<T>(x: T) { console.log(typeof T); } // error: cannot find name 'T'
-
keyof T
はどう変換されるか確認(→ ただの string) -
バベル経由ビルド(Next.js デフォルト)で “import 型削除” の挙動を追う
-
import type { Foo } from './types'
は Babel で跡形も残らない
-
6️⃣ よくある質問への ⚡️ショートアンサー
Q | A |
---|---|
instanceof は使えますか? |
クラスはコンストラクタが残るので OK。ただし interface / type alias には無力 |
typeof で generic の型判定できますか? |
不可能。値に降りてこない |
ブラウザで型情報取れないの? | 取れない。必要なら 独自メタデータ を埋め込むしかない(Reflect.metadata など) |
GraphQL Codegen と相性どう? | SDL → TS 型 → runtime バリデーションも同時生成できるので理想的 |
7️⃣ すぐ試せるミニチャレンジ 💡
- “型パラメータをログに出す” を試み → tsc エラーを読んで理解
-
zod
でネストした API レスポンスを validate → parse 成功後のみ 型が絞られる体験を確認 -
Next.js Route Handler で
POST /api/user
を受け取り → スキーマ検証して 400 を返すコードを書く
🚀 まとめ
-
TypeScript の型は “完全にコンパイル時専用”。0 byte で JS に残らない。
-
メリット:実行時オーバーヘッド皆無、JS との互換 100 %。
-
デメリット:ランタイム保証は自前で用意しないと “動くのに壊れる”。
-
実務指針:
-
外部 I/O 境界では スキーマ + 型生成(
zod
, OpenAPI, GraphQL Codegen)。 - 内部ロジックは “赤波線ゼロ” の静的型でバグを潰す。
-
CI に
tsc --noEmit
&tsd
を組み込み “消えた型” が守ってくれる範囲を常に可視化。
-
外部 I/O 境界では スキーマ + 型生成(
この “型消去のギャップ” を理解できれば、TypeScript = バンドルサイズゼロのテスト という真価を最大限引き出せます。

5
## 0️⃣ “ユニオン / インターセクション型” を一言で
記号 | 読み | イメージ | 主な用途 |
---|---|---|---|
A | B |
Union | 「A か B のどちらか」 (OR) | 状態遷移・バリアント表現・エラーハンドリング |
A & B |
Intersection | 「A も B も両方」 (AND) | Mixin 的合成・既存型の拡張・高階関数の型合成 |
1️⃣ Union 型:“どれか一つ” を静的に保証する
✨ Discriminated Union で状態機械を安全に
type Idle = { state: "idle" };
type Loading = { state: "loading" };
type Success = { state: "success"; data: string };
type Failure = { state: "failure"; error: Error };
type FetchState = Idle | Loading | Success | Failure;
function render(s: FetchState) {
switch (s.state) {
case "idle": return <button>Load</button>;
case "loading": return <p>Loading...</p>;
case "success": return <p>{s.data}</p>;
case "failure": return <p>{s.error.message}</p>;
default: /** exhaustiveness check */
const _exhaustive: never = s; // 🚨 新バリアントを追加したらここで検知
return _exhaustive;
}
}
ポイント
-
共通キー
state
をタグにすることで 型絞り込み (switch
) が効く。 -
never
チェックで “追加し忘れ” を即検知=将来の改修でも安全。
✨ 例:API 戻り値の Result パターン
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
async function fetchJSON<T>(url: string): Promise<Result<T>> { … }
- 呼び出し側は
if (res.ok)
で安全に分岐。 -
例外を投げずに型で分岐するスタイルは Rust の
Result
と同等の堅牢さ。
2️⃣ Intersection 型:“両方満たす” 特性合成
✨ Mixin 的に機能を盛る
type Draggable = { drag: () => void };
type Droppable = { drop: () => void };
type DragSource = Draggable & Droppable;
const card: DragSource = {
drag() { /* ... */ },
drop() { /* ... */ },
};
- 実装レベルの多重継承を避け、型だけ を合成できる。
✨ React × MUI でよく使う “Link as Button” 例
import Link, { LinkProps } from "next/link";
import Button, { ButtonProps } from "@mui/material/Button";
type LinkButtonProps = LinkProps & ButtonProps;
export const LinkButton = (props: LinkButtonProps) => (
<Button component={Link} {...props} />
);
-
&
で Next.js Link + MUI Button の両 API をそのまま公開。 - 継承より安全・余計なラップ不要 で DX 高し。
3️⃣ 🤝 Union × Intersection の合わせ技
技 | コード例 | 何が嬉しいか |
---|---|---|
分岐後に intersection で共有処理 | function handle(s: Success & LoggedIn) { … } |
成功 & 認証済みだけ通す |
ジェネリックで交差制約 | function assign<T extends A & B>(x:T){…} |
“A かつ B を満たす汎用 API” |
4️⃣ よくある落とし穴と防御策
落とし穴 | 例 | 防御策 | ||
---|---|---|---|---|
Union の optional 錯覚 | `type X = {a:number} | {b:number}→ x.a` 可能? |
in ガード ("a" in x ) で絞る |
||
プロパティ衝突 (Intersection) |
{x:number} & {x:string} → x は never
|
衝突しない型に分割、またはジェネリックで統制 | ||
巨大 Union の保守 | `"a" | "b" | …` 20個… |
as const 配列 + typeof arr[number] で生成 |
||
Union 配下の共通演算 |
map /filter に union 入れると inference 失敗 |
Conditional Type で分配 (T extends any ? … : … ) |
5️⃣ 実戦スニペット:フォーム入力の型安全変換
type RawField = { id: string; value: string };
type StringField = RawField & { kind: "text" };
type NumberField = RawField & { kind: "number" };
type Field = StringField | NumberField;
function parseField(f: Field) {
return f.kind === "number" ? Number(f.value) : f.value; // 型が絞られる
}
const fields: Field[] = [
{ id: "age", kind: "number", value: "42" },
{ id: "title", kind: "text", value: "Hello" },
];
- Union + Discriminant で 入力種別ごとの処理を安全に。
-
交差
& RawField
で 共通フィールドの重複を排除。
6️⃣ すぐ試せるミニチャレンジ 🔧
-
“ペット” モデルを
Dog | Cat | Fish
で作り、speak()
を実装。 -
Drag + Selectable を交差して
Resizable & Selectable
は 禁止 にする型を考案。 -
type Flatten<T>
を Union 分配+再帰で実装し、[[1,2],[3]]
→1|2|3
を型で得る。
🚀 まとめ
- Union:排他的バリアント → タグ付き & 専用ガード で 安全 + 保守容易。
- Intersection:横串で機能合成 → 継承より柔軟 に API を再利用。
- 合わせ技 で “状態 × 権限” など 多次元モデル も型安全に表現可能。

6
0️⃣ “リテラル型 & テンプレートリテラル型” を 30 秒で
観点 | 要点 |
---|---|
リテラル型 |
値そのもの ("GET" , 42 , true ) を型に昇格 → ユニオンで “許可値ホワイトリスト” を宣言できる |
テンプレートリテラル型 | バッククオート \`${A}-${B}\` で 文字列型を動的合成。パターンマッチ ( infer ) で 逆方向 の「文字列分解」も可能 |
効能 | フリーテキストの typo をビルド時に排除、ルーティングやイベント名など “構造ある文字列” を安全に扱える |
1️⃣ リテラル型の基本 – “値 = 型”
1-1. リテラルユニオンで許可値を閉じる
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
function request(method: HttpMethod, url: string) { /* ... */ }
request("PATCH", "/api"); // ❌ コンパイルエラー
as const
で “広がり” を防ぐ
1-2. const directions = ["up", "down"] as const;
// ^? readonly ["up","down"]
type Direction = typeof directions[number]; // "up" | "down"
- 配列 → リテラルユニオン を 自動生成 できる便利パターン。
2️⃣ テンプレートリテラル型 – “型で文字列を生成・解析”
2-1. 静的にパスを組み立てる
type Locale = "en" | "ja";
type Page = "home" | "about";
type Route = `/${Locale}/${Page}`; // "/en/home" | "/en/about" | …
const route: Route = "/ja/contact"; // ❌ 型エラー
2-2. パターンマッチで“型レベル split”
type SplitLocale<T> =
T extends `/${infer L}/${infer Rest}`
? L // "en" | "ja"
: never;
type L = SplitLocale<"/ja/about">; // "ja"
-
infer
と組合わせて 型安全な文字列パーサ を構築可能。
2-3. キーの自動生成 (映射型と併用)
type EventName = "click" | "hover";
type Side = "left" | "right";
type Combined = `${EventName}-${Side}`; // "click-left" | "hover-right" | …
type Handlers = { [K in Combined]: () => void };
- Cartesian Product を型で表現 → 実装コードは自動補完に乗る。
3️⃣ React / Next.js 実践スニペット
3-1. Next.js App Router の動的セグメント型
type SlugParam<T extends string> = {
params: { slug: T };
};
export async function generateStaticParams(): Promise<
SlugParam<`post-${number}`>[]
> {
return [{ params: { slug: "post-42" } }];
}
-
slug
がpost-${number}
形式のみ許容 → ビルド時に検知。
3-2. MUI Variant Props をリテラル型で定義
type Variant = "primary" | "secondary";
interface ButtonProps {
variant?: Variant;
}
const Button = ({ variant = "primary" }: ButtonProps) => (
<button className={`btn-${variant}`}>OK</button>
);
- デザインシステム側で 増減が頻繁な属性 を リテラルユニオン に集約すると破壊的変更を検出しやすい。
4️⃣ 逆方向:テンプレートリテラル型で Validate
type HexColor = `#${string}`;
function isHex(c: string): c is HexColor {
return /^#[\da-f]{6}$/i.test(c);
}
- Runtime Regex と Type Guard を組み合わせ → 入力値に型を付与。
5️⃣ 落とし穴 & ベストプラクティス
罠 / 痛点 | 例 | 対策 |
---|---|---|
ユニオン肥大 | 100 個超の "code01" … |
配列 as const → typeof arr[number] で自動生成 |
widening (幅拡張) | const method = "GET"; // string |
as const または satisfies HttpMethod
|
テンプレ型のネスト infer 地獄 | 再帰分解で無限ループ | TS 5.4 tail 制限 or 別の設計へ |
Regex ≠ Type |
type Hex = '#'+string は長さまで保証しない |
Type Guard + Regex で実行時も補完 |
6️⃣ すぐ試せるミニチャレンジ 🔧
-
type RGB =
rgb(${number},${number},${number})`` を作り、
extractRed<T>
で 1 番目の数値を取り出す型パズル。 -
REST API ルートを型で列挙 –
"GET /users"
などを
`${"GET"|"POST"} /${"users"|"posts"}`
で表現し、
fetchRoute<T>
関数が 適切な戻り型 を返すよう Conditional Type で実装。 -
フォームバリデーション –
FieldId = \
field-${number}`型を作り、 抽出した番号を
zod.number().min(1).max(99)` で runtime validate。
🚀 まとめ
- リテラル型 は “値の列挙”、テンプレートリテラル型 は “構造ある文字列”。
- どちらも “自由文字列の穴” を塞ぎ、IDE 補完 × 型安全 を両立。
-
as const
+ 配列 → ユニオン自動生成、infer
で分解 が王道テクニック。

7
0️⃣ “条件型 & 型推論ツール” を 30 秒で
観点 | 要点 |
---|---|
条件型 (Conditional Type) |
T extends U ? X : Y ― 型レベルで if / else を実行して 型を変形 する仕組み |
分配的条件型 | 裸の T がユニオンのとき 各要素に分配 → “ユニオンをまたいで走る map” として利用 |
infer キーワード |
条件式の中で 部分型をパターンマッチして取り出す (“型版正規表現のキャプチャ”) |
ツール例 |
ReturnType<T> , Awaited<T> , Parameters<T> など 公式 Utility 型はほぼ条件型で実装
|
1️⃣ 条件型の基本 – “型で if / else”
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<"abc">; // "yes"
type B = IsString<42>; // "no"
-
extends
で 型包含関係 を判定。 - 戻りは リテラル型 なので、さらにユニオンと組めば分岐に使える。
2️⃣ 分配的条件型 と “型マッピング”
type ToPromise<T> = T extends any ? Promise<T> : never;
type C = ToPromise<string | number>;
// ↑ Promise<string> | Promise<number>
- 裸の
T
がユニオン → 個々に展開 して再合成。 - 実質 “型版 map<Uni, F>” なのでユーティリティ生成に便利。
infer
で “型キャプチャ”
3️⃣ 3-1. 配列要素の抽出
type ElementOf<T> = T extends (infer U)[] ? U : never;
type D = ElementOf<string[]>; // string
3-2. 関数戻り値の抽出
type FnReturn<T> = T extends (...args: any) => infer R ? R : never;
type E = FnReturn<() => Promise<number>>; // Promise<number>
-
infer R
が “マッチした部分を取り出す” キャプチャ変数。 - 条件型の true ブランチ でのみ有効。
4️⃣ 実務ユースケース
4-1. API Result 型の “成功だけ取り出す”
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
type UnwrapResult<R> = R extends Ok<infer U> ? U : never;
type F = UnwrapResult<Result<number>>; // number
PartialDeep<T>
を 3 行で
4-2. type PartialDeep<T> =
T extends object ? { [K in keyof T]?: PartialDeep<T[K]> } : T;
- 再帰 + 条件型 で “任意の深さを optional” に。
4-3. React Prop “as” パターン
type PolymorphicProps<
T extends React.ElementType,
P = {}
> = P & { as?: T } & React.ComponentPropsWithoutRef<T>;
function Box<T extends React.ElementType = "div">(
props: PolymorphicProps<T>
) { /* ... */ }
-
ComponentPropsWithoutRef<T>
は 条件型で T が class / function かを分岐し、適切な props を注入してくれている。
5️⃣ 型推論ツール ― “型をテストするライブラリ”
ツール | 使い方 | 何ができるか |
---|---|---|
tsd |
.test-d.ts に expectType<Expected>(value)
|
CI で型の public API をスナップショット |
vitest-typings / expect-type
|
expectTypeOf(foo).toEqualTypeOf<Bar>() |
Unit Test と同列に 型の回帰テスト |
satisfies (TS 4.9+) |
const conf = obj satisfies Config |
値を narrow せず 構造チェックのみ 行う演算子 |
6️⃣ 落とし穴 & 防御策
痛点 | 例 | ガード |
---|---|---|
条件型の“分配し過ぎ” |
T extends any パターンで巨大型爆発 |
T extends unknown に変更し分配停止 |
無限再帰 | 自作 Flatten<T> で循環 |
TS 5.4 noUncheckedIndexedAccess , or 深さ制限 |
infer 使いすぎで可読性↓ |
ネスト infer 3 段以上 | 段階的 Helper 型を切り出す |
型テスト不足で silently break | 条件型を変更→下流ユーティリティ破壊 |
tsd を CI に追加 |
7️⃣ すぐ試せるミニチャレンジ 🔧
-
type First<T>
を書き、First<[3,5,7]> // 3
になるか確認。 -
オブジェクトから Nullable キーだけ抽出
type NullableKeys<T> = { [K in keyof T]-?: null extends T[K] ? K : never }[keyof T];
-
文字列から “id 数字” を推論
type ExtractId<S extends string> = S extends `id:${infer N extends number}` ? N : never;
🚀 まとめ
-
条件型 = “型の if 文”、
infer
= “型のキャプチャ”。 - 組み合わせると 型レベル DSL が書け、ユーティリティ型や型テスト を自作できる。
- React/Next.js でも Polymorphic Components・API DTO 変換 などで威力を発揮。

8
0️⃣ “マップド型 / Utility Types” を 30 秒で
観点 | 要点 |
---|---|
マップド型 (Mapped Type) |
type M<T> = { [K in keyof T]: … } ─ キー集合を走査し “型で map” する構文 |
修飾子 |
readonly / ? を + 追加 - 削除 → { [K in keyof T]-?: … }
|
キー再マップ |
… in keyof T **as** Camel<K> で 名前を変えて再生成
|
Utility Types | 公式 Partial / Pick / Omit / Record / Readonly / Required 等は すべてマップド型の組合せ
|
1️⃣ “キーをなぞる” 基本パターン
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
interface User {
readonly id: string;
name: string;
}
type WritableUser = Mutable<User>;
// { id: string; name: string }
-
-readonly
で 修飾子を剥がす。 -
keyof T
→"id" | "name"
のユニオンを走査。
2️⃣ Optional / Required の実装イメージ
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type P = MyPartial<{a:number; b:string}>; // {a?:number; b?:string}
type R = MyRequired<{a?:number}>; // {a:number}
-
?
を付与 / 剥がし だけで公式 Utility を再現。
as
でリネーム生成
3️⃣ キー“改名” – type PrefixKeys<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K]
};
type Original = { id: number; name: string };
type WithApi_ = PrefixKeys<Original, "api_">;
// { api_Id: number; api_Name: string }
- テンプレートリテラル型 と組むと API DTO ↔ 内部モデル の変換を型で保証。
4️⃣ 条件型 × マップド型 で “Deep Filter”
type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T]; // ✨ インデックスアクセスでユニオンへ
type PropsOnly<T> = Pick<T, NonFunctionKeys<T>>;
interface Comp { onClick(): void; title: string; }
type P = PropsOnly<Comp>; // { title: string }
- 中間型を
never
or キー にしてから[keyof T]
で抽出するのが定石。
5️⃣ 実務ユースケース
5-1. フォーム値 ↔ バリデーションエラー を自動対応
type FormValues = { email: string; password: string };
type FormErrors<T> = { [K in keyof T]?: string };
type Errors = FormErrors<FormValues>; // { email?: string; password?: string }
5-2. Next.js API Response DTO の “Deep Readonly”
type DeepReadonly<T> =
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
type Payload = { user:{ id:number; name:string } };
type Frozen = DeepReadonly<Payload>;
// user: { readonly id: number; readonly name: string }
6️⃣ 公式 Utility 型 “超” 早見表(一部)
型 | 実装イメージ | 典型用途 |
---|---|---|
Partial<T> |
{ [K in keyof T]?: T[K] } |
PATCH DTO |
Required<T> |
{ [K in keyof T]-?: T[K] } |
strict object |
Readonly<T> |
{ readonly [K in keyof T]: T[K] } |
immutability |
Pick<T,K> |
{ [P in K]: T[P] } |
取得 SELECT |
Omit<T,K> |
Pick<T, Exclude<keyof T,K>> |
API Expose |
Record<K,T> |
{ [P in K]: T } |
id→value map |
7️⃣ よくある 落とし穴 と防御策
痛点 | 例 | ガード | |
---|---|---|---|
**keyof any で string |
number** | [K in keyof any] |
かならず 入力型で制限 |
-? と +? の混同 |
-? = 必須化 / +? = optional |
覚えにくければ Required<T> を分解して確認
|
|
再帰で交差爆発 | DeepReadonly 巨大型で遅く |
as unknown as で段階区切りor depth param |
|
ネスト optional の迷路 |
T[K]? と ?: の混在 |
TS5.0 exactOptionalPropertyTypes を true |
8️⃣ すぐ試せるミニチャレンジ 🔧
-
MutableKeys<T>
–readonly
プロパティだけ列挙。 -
SnakeToCamel<T>
–as
+ テンプレ型でキー改名し、API → UI 変換を自動化。 -
Merge<A,B>
–{ [K in keyof A | keyof B]: … }
で “後勝ち” マージを型で表現。
🚀 まとめ
- マップド型 = “キーを走査する for…in”、Utility 型は “型の APIKit”。
- 修飾子操作・キー再マップ・条件型の組合せ で 型変換 DSL が書ける。
- React/Next.js でも DTO↔ViewModel / FormErrors / DeepReadonly など 横断変換を自動化 し、バグ温床を撲滅。

9
0️⃣ “strict null checks & 逐次強化” を 30 秒で
観点 | 要点 |
---|---|
strict null checks |
undefined / null は あらゆる型のサブタイプではなくなる ── “nullable フラグ” を明示しないと代入エラー |
逐次強化 (gradual hardening) | 既存 JS/緩い TS に 段階的に安全網を張る 戦略: ① project-level → ② file-level → ③ 行単位 |
効果 | “動くけど NPE” クラスのバグを ビルド時に撲滅。DX と保守性を両立 |
1️⃣ まずは tsconfig を理解する
{
"compilerOptions": {
"strict": true, // ← 丸ごと有効
// 個別に切るなら:
// "strictNullChecks": true,
// "exactOptionalPropertyTypes": true,
// "noUncheckedIndexedAccess": true
}
}
オプション | 何が追加で厳しくなるか | 典型エラー例 |
---|---|---|
strictNullChecks |
T と T | undefined が別物 |
“Object is possibly ‘undefined’” |
exactOptionalPropertyTypes (4.4+) |
foo?: T は “無い” と “undefined 値が入る” を区別 |
パーシャルアップデートで漏れ検知 |
noUncheckedIndexedAccess |
配列/Record の要素取得は T | undefined
|
map し忘れ検知 |
2️⃣ コンパイラが怒る代表パターンと 安全修正レシピ
ケース | NG 例 | OK 例 | 解説 |
---|---|---|---|
1. 未定義かも | user.name.length |
user?.name?.length ?? 0 |
Optional Chaining / Nullish Coalescing でガード |
2. 関数戻り値を信用 | const d: Date = getMaybeDate(); |
const d = getMaybeDate()!; or runtime check |
! は “自信があるときだけ”。「型テスト」推奨 |
3. 配列要素 | todos[0].id |
todos.at(0)?.id |
noUncheckedIndexedAccess で未定義防止 |
4. Optional Props | <Avatar src={user.avatar}/> |
{user.avatar && <Avatar …/>} |
JSX でも null 選択 |
3️⃣ React / Next.js における典型シーン
3-1. 状態初期化
const [data, setData] = useState<User | null>(null);
// ^? User | null
if (!data) return <Spinner />;
- 初期値が null なら 型にも null を入れる。
- 取得後
data!
は OK だが render 分岐で解決するほうが安全。
3-2. useRef と Non-null 断言
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // ?. を忘れない
}, []);
3-3. Next.js loader / action
export async function loader({ params }: LoaderArgs) {
const post = await db.post.find(params.id);
if (!post) throw new Response("Not Found", { status: 404 });
return json(post); // post は non-null
}
- 404 で “null escape” させておくと後段が安心。
4️⃣ 逐次強化ロードマップ
フェーズ | やること | コツ |
---|---|---|
Step 0 |
"skipLibCheck": true で外部型は無視、まず自分のコード専念 |
CI だけ strict にしても OK |
Step 1 |
strictNullChecks: true 、noImplicitAny: true
|
“赤波線” を TODO コメントに逃がしても良い |
Step 2 | exactOptionalPropertyTypes: true |
DTO 差分が浮き彫り → 安全にリファクタ |
Step 3 | noUncheckedIndexedAccess: true |
.at() or length check を導入 |
Step 4 |
strict: true 一括 & eslint @typescript-eslint/no-non-null-assertion で ! の乱用抑止 |
運用 TIP:
// @ts-ignore[7023]
など ID 付き suppress を使うと “借金リスト” が grep しやすい。
5️⃣ ランタイム検証との“合わせ技”
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const user = UserSchema.parse(await res.json()); // runtime & compile OK
- strictNullChecks だけでは“外部データの null” は防げない → バリデーションで補完。
6️⃣ よくある 落とし穴 と対策
痛点 | 罠 | 対策 |
---|---|---|
non-null assertion乱用 | foo!.bar!.baz |
ESLint no-non-null-assertion , or 型テスト |
Optional property の “undefined 漏れ” |
{title?:string} なのに obj.title.trim()
|
exactOptionalPropertyTypes を有効化 |
外部ライブラリが未対応 | 型が any で警告増殖 |
@types/xxx@beta or 手書き .d.ts 、skipLibCheck
|
React Context default null | コンテキスト値読み取りで毎回 null check |
if (!ctx) throw new Error("…") guard → 型が絞れる |
7️⃣ すぐ試せるミニチャレンジ 🔧
-
“safeAccess” ヘルパ
const safe = <T, R>(v: T | null | undefined, fn: (x: T) => R) => v == null ? undefined : fn(v);
-
assertExists(value, msg?)
Type Guard を実装してvalue is NonNullable<T>
を返す。 -
tsc --noEmit
× Git hook で “strict null error が PR に乗らない” ワークフローを構築。
🚀 まとめ
- strictNullChecks = “null/undefined を型に昇格”。NPE は ビルド時に葬る。
- 段階的に
exactOptionalPropertyTypes
→noUncheckedIndexedAccess
を足して 安全域を拡張。 - React/Next.js では 初期値・Context・API フェッチ が主要 null 発生源。
- Type Guard + Runtime バリデーション を組み合わせると “外からの null” にも鉄壁。

10
0️⃣ “JS 互換と段階的導入” を 30 秒で
観点 | 要点 |
---|---|
100 % JS 互換 | TypeScript は “JS→JS 変換+型チェック”。拡張子変更以外の破壊は一切なし |
漸進移行 (Gradual Adoption) |
① 無変更 JS (.js ) → ② JSDoc + // @ts-check → ③ .ts/.tsx → ④ strict ON の 4 段階でリスクを極小化 |
ツールチェーン |
allowJs , checkJs , noEmit , isolatedModules , overrides で ファイル粒度 にポリシーを調整 |
1️⃣ ステップゼロ:現状を壊さず型チェックだけ掛ける
tsconfig.quick.json
{
"compilerOptions": {
"allowJs": true, // JS を入力に許可
"checkJs": false, // まずは無効 (後で段階的に ON)
"noEmit": true // JS を書き換えず型エラーだけ表示
},
"include": ["src"]
}
npx tsc -p tsconfig.quick.json # 型エラーゼロを確認
// @ts-check
で“型だけ”導入
2️⃣ ステップ1:JSDoc + // math.js
// @ts-check
/**
* @param {number} a
* @param {number} b
*/
export function add(a, b) {
return a + b;
}
add("1", 2); // 🚨 型エラー (string ↔ number)
JSDoc Tag | 例 | 対応 TS 型 |
---|---|---|
@type |
/** @type {HTMLDivElement} */ |
型アサーション |
@param / @returns |
@param {Foo[]} arr |
関数シグネチャ |
@template |
@template T @param {T} value @returns {T} |
ジェネリクス |
Tip: VS Code は JSDoc 型も IntelliSense 可能。既存 JS チームへ“無痛プレビュー” を提供。
.ts / .tsx
へ rename
3️⃣ ステップ2:拡張子 -
まず 宣言の少ない util ファイル から
.ts
に。 -
allowJs
は残しておき、JS ↔ TS を相互 import。 -
React なら
.jsx → .tsx
:JSX タグが型推論に乗る。
// Button.tsx
export interface ButtonProps {
label: string;
onClick?: () => void;
}
export const Button = ({ label, onClick }: ButtonProps) => (
<button onClick={onClick}>{label}</button>
);
strict
を段階的に ON
4️⃣ ステップ3:tsconfig.json (抜粋)
"compilerOptions": {
"strict": true,
"skipLibCheck": true
},
"overrides": [
{ "files": ["legacy/**/*"], "compilerOptions": { "strict": false } }
]
- overrides (TS 5.0+) で “古いままの領域” を緩和。
- CI では
tsc --noEmit
を走らせ 赤波線コミットをブロック。
5️⃣ リアルコードで見る“漸進リライト”
5-1. 既存 JS util (JSDoc stage)
// slugify.js (@ts-check)
/**
* @param {string} title
* @returns {string}
*/
export function slugify(title) {
return title.toLowerCase().replace(/\s+/g, "-");
}
5-2. TS 化 & 強化
// slugify.ts
export type Slug = `${string}-${string}`;
/** Converts a title to kebab-case slug. */
export function slugify(title: string): Slug {
return title
.trim()
.toLowerCase()
.replace(/\s+/g, "-") as Slug; // 型ブランド化
}
- テンプレリテラル型 で “slug 形式” を保証。
- 呼出元は
Slug
期待で typo 排除。
6️⃣ ランタイム互換:Babel / ts-node / ESM
環境 | 設定ポイント |
---|---|
Babel |
@babel/preset-typescript で 型を剥がすだけ
|
ts-node |
ts-node --transpileOnly → dev 速い、型エラーは別で tsc |
ES Modules |
"module": "esnext", "target": "es2020" + "type":"module" in package.json |
Next.js / Vite / Bun などは内部で swc / esbuild が TS を剥がしてくれるので追加設定は最小。
7️⃣ よくある 落とし穴 と対策
痛点 | 罠 | 対策 |
---|---|---|
JS / TS 循環 import |
.js → .ts rename後 relative path崩壊 |
先に baseUrl , paths で alias 化 |
型定義の収集漏れ | JS ライブラリが d.ts 未同梱 |
npm i -D @types/xxx or declare module
|
ビルドと型チェックの二重処理 | Babel + tsc で遅い |
Build: swc/esbuild, Check: tsc --noEmit
|
Gradual but forever | overrides が放置されゾンビ JS 増殖 | ガイドライン:毎週 10 file を TS 化 KPI |
8️⃣ すぐ試せるミニチャレンジ 🏃♂️
-
allowJs:true
で既存プロジェクトに tsc を当て、エラー件数を計測。 -
// @ts-check
を 1 つの util フォルダにだけ付け、最頻出エラー TOP3 を Slack 共有。 -
React
.jsx
→.tsx
へ 1 コンポーネント rename。props
型をsatisfies {}
で書いてみる。
🚀 まとめ
- TS は “脱 JS” ではなく “型付き JS”――既存コードを 壊さず 導入できる。
- JSDoc → rename → strict の 4 段階を踏むと リスク ≒ 1/N。
- overrides / allowJs / checkJs を駆使し、“今書くコードは常に型安全” を維持しながらレガシーを漸進解体。

11
## 0️⃣ “宣言ファイル / 型定義の分離” を 30 秒で
観点 | 要点 |
---|---|
宣言ファイル (*.d.ts ) |
値を一切出力せず “型情報だけ” を記述するファイル。JS 実装と物理的に分離できる。 |
3 つの用途 |
① 外部 JS ライブラリの型付け ② 既存型の拡張(Module Augmentation) ③ ライブラリ作者が公開 API をドキュメント化 |
導入パターン |
プロジェクト内: types/ に置き typeRoots で読むnpm 配布: "types" フィールド or @types/xxx
|
1️⃣ “JS 実装 + .d.ts” ― 既存ライブラリを安全に使う
1-1. JavaScript 本体 (peer-lib/timer.js)
export function delay(ms) {
return new Promise(r => setTimeout(r, ms));
}
1-2. 型宣言 (types/timer.d.ts)
declare module "peer-lib/timer" {
/** Resolves after at least `ms` milliseconds. */
export function delay(ms: number): Promise<void>;
}
1-3. tsconfig.json
{
"compilerOptions": { "typeRoots": ["./types", "./node_modules/@types"] }
}
import { delay } from "peer-lib/timer";
await delay("500"); // 🚨 string↔number エラー
- 値は JS から、型は .d.ts から 読み込まれる。
- チーム内 JS を “壊さずに” 型安全化。
2️⃣ Module Augmentation -- 既存型に“追記”
user
プロパティを注入
2-1. Express に // express-session.d.ts
import "express";
declare module "express" {
interface Request {
user?: { id: string; role: "admin" | "guest" };
}
}
app.get("/me", (req, res) => {
req.user.id // 型補完が効く
});
-
同名モジュールに
declare module "…" {}
を書くと マージ される。
3️⃣ グローバル宣言 の作法
// globals.d.ts
export {}; // (=モジュール化して衝突防止)
declare global {
type ID = `${string}-${string}-${string}`; // どこからでも ID 型が見える
}
-
export {}
を最上位に置き “ファイル自体はモジュール” と宣言 → 汚染を制御。
@types
4️⃣ ライブラリ作者向け:型を同梱 or 方式 | package.json | 適用例 |
---|---|---|
バンドル同梱 | "types": "./dist/index.d.ts" |
UI コンポーネント lib など TS で実装 |
.d.ts 手書き同梱 |
同上 | JS 実装+型だけ TS の場合 |
DefinitelyTyped | "name": "@types/lodash" |
OSS JS で作者 ≠ 型メンテ |
tsconfig for build
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./dist",
"composite": true // (モノレポで依存解決を速くする)
}
5️⃣ Next.js / MUI でよく使う宣言ファイル技
5-1. MUI Theme 拡張
// mui-theme.d.ts
import "@mui/material/styles";
declare module "@mui/material/styles" {
interface Palette {
neutral: Palette["primary"];
}
interface PaletteOptions {
neutral?: PaletteOptions["primary"];
}
}
-
JSX で
<Button color="neutral">
と書いた瞬間に型が通る。
next-env.d.ts
の役割
5-2. /// <reference types="next" />
/// <reference types="next/image-types/global" />
- triple-slash directive で 複数型パッケージを一括 import。
6️⃣ 型衝突を避ける“命名 4 ルール”
-
npm 名と同じ文字列で
declare module
-
相対パス Import のみ使うコードに対しては
./src/types/foo.d.ts
内部モジュール名も 相対で記述 - 重複した global interface 名は namespace prefix で差別化
-
再エクスポートするときは
export * from "…"
ではなく 選別 import で衝突防止
7️⃣ よくある 落とし穴 と対策
痛点 | 罠 | 対策 |
---|---|---|
値を宣言ファイルに書く |
export const x = 1; in .d.ts |
すべて declare (値を生成しない) |
非モジュール .d.ts が汚染 | 直接 interface Window {}
|
export {} を先頭に置く |
型循環でビルド遅延 | 互いに declare module し合う |
paths alias or barrel ファイル |
型が見つからない |
"types" 路指定ミス |
pnpm tsc --traceResolution で解決経路確認 |
8️⃣ すぐ試せるミニチャレンジ 🔧
-
自作 JS util に JSDoc 型 →
.d.ts
分離 を試し、“型エラー 0” が保てるか比較。 -
Axios Response 型の追加メタデータ (
cacheHit?:boolean
) を module augmentation で拡張。 - DefinitelyTyped の PR ガイドに沿って
@types/<your-pkg>
をローカルで生成し、CI (npm test
) を通す。
🚀 まとめ
- 宣言ファイルは 「実装ゼロの型パッケージ」。JS 資産・外部ライブラリ・既存型の拡張を 安全に合体 できる。
- コードベースでは
types/
ディレクトリ +typeRoots
、ライブラリでは"types"
フィールド or DefinitelyTyped。 - Module Augmentation & Global Declaration を駆使し、React/MUI/Next.js の 型 DX を一段引き上げる

12
0️⃣ “型安全 × DX(Developer Experience)の相関” を 30 秒で
観点 | 要点 |
---|---|
型安全 ⇒ 即時フィードバック | IDE 補完・エラーチェック・リネームリファクタ が 100 ms 未満で応答 ─ 開発サイクルが “Run → Fail” から “Type → Succeed” へ |
“赤波線ゼロ” = テストの前倒し | 型が仕様を“実行前にシミュレート” → ユニットテスト数を減らしながら品質↑ |
リファクタコストの指数抑制 | プロダクト成長に比例して増える 影響範囲調査 が Ctrl + Click / Rename で定数時間に短縮 |
コラボ DX | ペアプロ・PR レビューで“WHY”より前に“WHAT”が共有されるため 認知負荷↓ & onboarding 迅速 |
1️⃣ IDE が“型”を DX に変換するメカニズム
機能 | 具体例 | 型がどう貢献 |
---|---|---|
IntelliSense |
user. と打つと id , name 候補 |
構造的型情報 で正確 & 重複ゼロ |
リネームセーフ |
F2 で addUser →createUser
|
使用箇所を静的解析、文字列・コメントは除外 |
ジャンプ / 参照検索 | Go to Definition |
型グラフですばやく解決 |
自動 import |
format() と書くだけで import { format } from "date-fns";
|
型解決済みシンボル を優先候補 |
エラー抑制ヒント |
?. や ! 提案 |
nullability 情報 をトラッキング |
2️⃣ 実例:“any → 型安全” で DX がどう変わるか
any
地獄
2-1. before – // userService.ts
export function getUser(id: string): any {
return fetch(`/api/users/${id}`).then(r => r.json());
}
const user = await getUser("42");
console.log(user.namme.toUpperCase()); // typo! => runtime error
2-2. after – 型付き & バリデーション
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
export type User = z.infer<typeof UserSchema>;
export async function getUser(id: string): Promise<User> {
const json = await fetch(`/api/users/${id}`).then(r => r.json());
return UserSchema.parse(json); // runtime+compile
}
// 使う側
const user = await getUser("42");
console.log(user.name.toUpperCase()); // 補完 OK / typo 即赤波線
結果
- 補完が期待通り → コーディング速度↑
- typo で即エラー → QA コスト↓
- User 型がドキュメント代わり → コードリーディング時間↓
3️⃣ 型安全が CI/CD に与えるインパクト
パイプライン段 | 型安全が担うテスト | 従来のスクリプトテスト |
---|---|---|
Lint | 構文 + 型ルール (eslint-plugin-ts ) |
JSDoc スタブ |
Build |
tsc --noEmit で API 互換チェック |
E2E 回さないと検出不可 |
PR Review | “エラー 0” = レビューはロジック集中 | 実装+静的チェックを人力で確認 |
Release |
d.ts が 公開 API の自動ドキュメント |
Changelog 手書き |
→ デプロイ前のバグ混入率が 1 桁減(実案件で 5→0.3 % などの報告多数)
4️⃣ DX をブーストする型ツールチェーン
ツール | 役割 | 典型設定 |
---|---|---|
tsserver | VSCode / WebStorm に型サービス提供 | "typescript.tsdk":"node_modules/typescript/lib" |
ESLint + @typescript-eslint |
コーディング規約 + 型解析 | parserOptions.project: ["tsconfig.json"] |
Prettier | フォーマット一元化(型関係ないが DX 必須) | "pluginSearchDirs":false |
ts-node / SWC / esbuild | ts → js トランスパイル高速化 |
transpileOnly フラグでビルド別実行 |
tsd / expect-type | 型の単体テスト | npm run test:types |
5️⃣ “型 & DX” 成熟度チェックリスト
レベル | 指標 | 達成策 |
---|---|---|
Lv1 |
tsc --noEmit が CI パス |
allowJs + skipLibCheck |
Lv2 | PR 時 ESLint 型ルール全パス | @typescript-eslint/no-explicit-any |
Lv3 | public API 変更は d.ts diff で検知 |
api-extractor |
Lv4 | IDE Rename で Test 通過率 95 %→99 % | 型テスト + strictNullChecks |
Lv5 | コードジェネレータで DTO ⇄ Schema 自動同期 | OpenAPI / GraphQL codegen |
6️⃣ よくある 反論 & 説明テンプレ
| “型は遅い” | ts-patch + SWC で 10×ビルド、型検証は CI へ分離 |
| “学習コスト高” | 3 段階 (any→strictNull→utility) ロードマップ提示 |
| “Insider Only” | .d.ts
は JS チームへも補完提供 → 全員恩恵 |
7️⃣ すぐ試せるミニチャレンジ 🏃♀️
-
rename vs grep ベンチ
-
id
→userId
を IDE Rename と VSCode 全文検索で速度比較し数値化。
-
-
型テストスナップショット
npx tsd # fail0? → commit gate
-
PR テンプレに “tsc –noEmit success ✔️” チェックボックス を追加してみる。
🚀 まとめ
- 型安全は DX を“体感スピード・安全・可読性”の三方向で底上げするレバレッジ。
- IDE 機能・CI パイプライン・自動ドキュメント化が “書く前・動かす前” にバグを除去し、開発者の思考帯域をビジネスロジックに集中させる。
- 赤波線ゼロ文化 と 型テスト を組み合わせ、コードベースの成長とともに 精神的負債 を逆に減らす組織へ。