Open13

Building TypeScript

T-unityT-unity

0

TypeScript を「他の一般的な言語」と比べた 12 の特徴と観点

# TypeScript ならではの観点 ざっくり要点 他言語との主な違い / 似ている概念
1 構造的型付け(Structural Typing) “形が同じなら同じ型”
名前や継承階層ではなくプロパティ構造で互換判定
Go も近いが、Java/C# は名義(Nominal)型付け
2 型注釈(Type Annotation) : numberas 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 言語の感覚に似つつ、ブラウザ即確認の速さ

インサイト & 立ち止まるポイント

  1. “型はコードの圧縮表現” – ロジックを型に持ち上げると実装コードがシンプル化しテスト負荷も減る。
  2. ランタイム検証ギャップ – 型消去ゆえに zod, io-ts など 実行時スキーマ が要るケースを意識。
  3. パフォーマンス vs 型安全 – 型演算はビルド時コストになる。巨大プロジェクトでは skipLibCheck, isolatedModules のトレードオフを検討。
  4. “赤波線ゼロ” を保つ文化 – ESLint no-implicit-any, @typescript-eslint/consistent-type-imports 等で 型逸脱を早期ブロック
  5. ユーティリティ型の自作 – プロジェクト固有ドメインを type ID = Branded<string, "ID"> のように“単位付き”化するとバグ激減。

具体的アプローチ 3 通り

アプローチ どう進めるか 向いているケース
① TDD ↔ Type-Driven Design “テスト前に型で仕様を表す”。tsd で型の単体テストを書く API/ライブラリ設計
② JS → TS 漸進移行 // @ts-check + JSDoc 型記法 → .ts 化 → strict 強化 既存 JS 資産が多い
③ 型レベルプログラミング学習 Playground で条件型パズルを解く。“型で Fibonacci” など アルゴリズム好き・型厨

まとめ

TypeScript の真価は 「JS の柔軟さ × 静的型の安心感」 を“後付け”できる点にあります。
構造的型付け・型注釈・ジェネリクスなど豊富な型機能を活かし、「まず動かす」から 「型で壊れない設計」 へ一段上げるのが到達目標です。

T-unityT-unity

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最低限 nameage を持つので 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>
  );
};
  • PrimaryButtonSecondaryButton構造的に共通部分 を合成。
  • さらに 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️⃣ “型安全 × 柔軟” を両立させる設計指針

  1. “外部境界”では Brand / Discriminated Union で厳密化

    • API 入力・DB ID など 違うけど形が同じ ものは区別する。
  2. “内部流通”は構造的利便性を活かし最小限のフィールドで伝搬

    • DTO → ViewModel と段階的に 型を絞る とテスト容易。
  3. exactOptionalPropertyTypestrue(TS 4.4+)

    • “あるかも・無いかも” をより厳しく判定でき、思わぬ互換を防ぐ。
  4. 型テスト (tsd, expectTypeOf) を CI に組み込む

    • コンパイル通過だけでなく 想定した互換 を自動チェック。

7️⃣ すぐ試せるミニチャレンジ 🏃‍♂️

  1. ブランド型で UserID ↔ Email を区別しつつ
    map<UserID, User> のキーに使ってみる。

  2. 関数互換クイズ – 以下が assignable かどうかを予想 → tsc で検証

    type F1 = (a: number, b: number) => void;
    type F2 = (a: number) => void;
    let f: F1 = F2 ??? // OK or Error?
    
  3. 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, 状態遷移 を “形+タグ” で設計すると
    テスト工数とバグ混入が激減
T-unityT-unity

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リテラル型 へ昇格 → 誤代入を防止。

1-4. 型アサーション (as)

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 / [] だけだと推論が弱い → 注釈必須。

3-3. useRef の Non-null 記法

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️⃣ すぐ試せるミニチャレンジ 🏃‍♀️

  1. “引数が増えたらエラー” テスト

    function greet(person: {name:string}) {}
    // 呼び出し側で name typo を起こしてみる
    
  2. Readonly<T>as const の違い をコードで比較。

  3. 初期値が空配列の useState で推論を観察し、型引数の有無で IDE 補完がどう変わるか確認。


🚀 まとめ

  • 型注釈 = コンパイラと未来の自分へのメッセージ
  • 境界にだけ書く/内部は推論に任せる が “読みやすさ×安全” の黄金比。
  • React/Next.js では Props・Hook 初期値・Ref が 3 大注釈ポイント。

👉 自分のプロジェクトで「暗黙 any」「広がりすぎ string」 を検索し、
必要十分な注釈を追加して “赤波線ゼロ & すっきりコード” を体感してみてください!

T-unityT-unity

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 なので 型安全に再利用できる。

2️⃣ 制約付きジェネリクス<T extends …>

✨ プロパティ存在を保証して安全にアクセス

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️⃣ すぐ試せるミニチャレンジ 🔧

  1. “配列をフラット化する型” を実装

    type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
    
  2. pick<T, K extends keyof T> を手書きで作り、Pick と差を比べる。

  3. React.ComponentType<P> 相当を実装し、HOC の理解を深める。


🚀 まとめ

  • ジェネリクスは “型の関数化”再利用性 × 安全性 を同時に高める鍵。
  • 制約 (extends)・キー制約 (keyof)・デフォルト型 を組み合わせ、抽象度と実用性を両立。
  • React / Next.js でも 汎用 UI・HOC・API ラッパー など至る所で威力を発揮。
T-unityT-unity

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

3-2. スキーマ → 型自動生成zod, @sinclair/typebox, yup…)

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

3-3. コンパイル時のみ型検証するユーティリティ (satisfies)

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.valuestring 固定 型レベルで絞り込む → runtime で parse (Number(), Date)

5️⃣ ジェネリックの“消え方”を突き詰める 3 ステップ

  1. Playground で Tconsole.log してみる

    function foo<T>(x: T) { console.log(typeof T); } // error: cannot find name 'T'
    
  2. keyof T はどう変換されるか確認(→ ただの string)

  3. バベル経由ビルド(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️⃣ すぐ試せるミニチャレンジ 💡

  1. “型パラメータをログに出す” を試み → tsc エラーを読んで理解
  2. zod でネストした API レスポンスを validateparse 成功後のみ 型が絞られる体験を確認
  3. Next.js Route HandlerPOST /api/user を受け取り → スキーマ検証して 400 を返すコードを書く

🚀 まとめ

  • TypeScript の型は “完全にコンパイル時専用”0 byte で JS に残らない。

  • メリット:実行時オーバーヘッド皆無、JS との互換 100 %。

  • デメリット:ランタイム保証は自前で用意しないと “動くのに壊れる”

  • 実務指針

    1. 外部 I/O 境界では スキーマ + 型生成zod, OpenAPI, GraphQL Codegen)。
    2. 内部ロジックは “赤波線ゼロ” の静的型でバグを潰す。
    3. CI に tsc --noEmit & tsd を組み込み “消えた型” が守ってくれる範囲を常に可視化。

この “型消去のギャップ” を理解できれば、TypeScript = バンドルサイズゼロのテスト という真価を最大限引き出せます。

T-unityT-unity

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() { /* ... */ },
};
  • 実装レベルの多重継承を避け、型だけ を合成できる。
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}xnever 衝突しない型に分割、またはジェネリックで統制
巨大 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️⃣ すぐ試せるミニチャレンジ 🔧

  1. “ペット” モデルを Dog | Cat | Fish で作りspeak() を実装。
  2. Drag + Selectable を交差して Resizable & Selectable禁止 にする型を考案。
  3. type Flatten<T>Union 分配+再帰で実装し、[[1,2],[3]]1|2|3 を型で得る。

🚀 まとめ

  • Union:排他的バリアント → タグ付き & 専用ガード安全 + 保守容易
  • Intersection:横串で機能合成 → 継承より柔軟 に API を再利用。
  • 合わせ技 で “状態 × 権限” など 多次元モデル も型安全に表現可能。
T-unityT-unity

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");   // ❌ コンパイルエラー

1-2. as const で “広がり” を防ぐ

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" } }];
}
  • slugpost-${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 RegexType 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️⃣ すぐ試せるミニチャレンジ 🔧

  1. type RGB = rgb(${number},${number},${number})`` を作り、
    extractRed<T> で 1 番目の数値を取り出す型パズル。
  2. REST API ルートを型で列挙"GET /users" などを
    `${"GET"|"POST"} /${"users"|"posts"}` で表現し、
    fetchRoute<T> 関数が 適切な戻り型 を返すよう Conditional Type で実装。
  3. フォームバリデーションFieldId = \field-${number}`型を作り、 抽出した番号をzod.number().min(1).max(99)` で runtime validate。

🚀 まとめ

  • リテラル型“値の列挙”テンプレートリテラル型“構造ある文字列”
  • どちらも “自由文字列の穴” を塞ぎ、IDE 補完 × 型安全 を両立。
  • as const + 配列 → ユニオン自動生成infer で分解 が王道テクニック。
T-unityT-unity

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>” なのでユーティリティ生成に便利。

3️⃣ infer で “型キャプチャ”

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

4-2. PartialDeep<T> を 3 行で

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.tsexpectType<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️⃣ すぐ試せるミニチャレンジ 🔧

  1. type First<T> を書き、First<[3,5,7]> // 3 になるか確認。

  2. オブジェクトから Nullable キーだけ抽出

    type NullableKeys<T> = {
      [K in keyof T]-?: null extends T[K] ? K : never
    }[keyof T];
    
  3. 文字列から “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 変換 などで威力を発揮。
T-unityT-unity

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 を再現。

3️⃣ キー“改名”as でリネーム生成

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️⃣ すぐ試せるミニチャレンジ 🔧

  1. MutableKeys<T>readonly プロパティだけ列挙。
  2. SnakeToCamel<T>as + テンプレ型でキー改名し、API → UI 変換を自動化。
  3. Merge<A,B>{ [K in keyof A | keyof B]: … } で “後勝ち” マージを型で表現。

🚀 まとめ

  • マップド型 = “キーを走査する for…in”、Utility 型は “型の APIKit”
  • 修飾子操作・キー再マップ・条件型の組合せ型変換 DSL が書ける。
  • React/Next.js でも DTO↔ViewModel / FormErrors / DeepReadonly など 横断変換を自動化 し、バグ温床を撲滅。
T-unityT-unity

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 TT | 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: truenoImplicitAny: 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.tsskipLibCheck
React Context default null コンテキスト値読み取りで毎回 null check if (!ctx) throw new Error("…") guard → 型が絞れる

7️⃣ すぐ試せるミニチャレンジ 🔧

  1. “safeAccess” ヘルパ

    const safe = <T, R>(v: T | null | undefined, fn: (x: T) => R) =>
      v == null ? undefined : fn(v);
    
  2. assertExists(value, msg?) Type Guard を実装して value is NonNullable<T> を返す。

  3. tsc --noEmit × Git hook で “strict null error が PR に乗らない” ワークフローを構築。


🚀 まとめ

  • strictNullChecks = “null/undefined を型に昇格”。NPE は ビルド時に葬る
  • 段階的に exactOptionalPropertyTypesnoUncheckedIndexedAccess を足して 安全域を拡張
  • React/Next.js では 初期値・Context・API フェッチ が主要 null 発生源。
  • Type Guard + Runtime バリデーション を組み合わせると “外からの null” にも鉄壁。
T-unityT-unity

10

0️⃣ “JS 互換と段階的導入” を 30 秒で
観点 要点
100 % JS 互換 TypeScript は “JS→JS 変換+型チェック”。拡張子変更以外の破壊は一切なし
漸進移行 (Gradual Adoption) ① 無変更 JS (.js)② JSDoc + // @ts-check.ts/.tsxstrict 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    # 型エラーゼロを確認

2️⃣ ステップ1:JSDoc + // @ts-check で“型だけ”導入

// 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 チームへ“無痛プレビュー” を提供。


3️⃣ ステップ2:拡張子 .ts / .tsx へ rename

  1. まず 宣言の少ない util ファイル から .ts に。
  2. allowJs は残しておき、JS ↔ TS を相互 import
  3. React なら .jsx → .tsx:JSX タグが型推論に乗る。
// Button.tsx
export interface ButtonProps {
  label: string;
  onClick?: () => void;
}
export const Button = ({ label, onClick }: ButtonProps) => (
  <button onClick={onClick}>{label}</button>
);

4️⃣ ステップ3:strict を段階的に ON

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️⃣ すぐ試せるミニチャレンジ 🏃‍♂️

  1. allowJs:true で既存プロジェクトに tsc を当て、エラー件数を計測。
  2. // @ts-check を 1 つの util フォルダにだけ付け、最頻出エラー TOP3 を Slack 共有。
  3. React .jsx.tsx へ 1 コンポーネント rename。props 型を satisfies {} で書いてみる。

🚀 まとめ

  • TS は “脱 JS” ではなく “型付き JS”――既存コードを 壊さず 導入できる。
  • JSDoc → rename → strict の 4 段階を踏むと リスク ≒ 1/N
  • overrides / allowJs / checkJs を駆使し、“今書くコードは常に型安全” を維持しながらレガシーを漸進解体。
T-unityT-unity

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 -- 既存型に“追記”

2-1. Express に user プロパティを注入

// 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 {} を最上位に置き “ファイル自体はモジュール” と宣言 → 汚染を制御。

4️⃣ ライブラリ作者向け:型を同梱 or @types

方式 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"> と書いた瞬間に型が通る。

5-2. next-env.d.ts の役割

/// <reference types="next" />
/// <reference types="next/image-types/global" />
  • triple-slash directive複数型パッケージを一括 import

6️⃣ 型衝突を避ける“命名 4 ルール”

  1. npm 名と同じ文字列declare module
  2. 相対パス Import のみ使うコードに対しては ./src/types/foo.d.ts 内部モジュール名も 相対で記述
  3. 重複した global interface 名namespace prefix で差別化
  4. 再エクスポートするときは 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️⃣ すぐ試せるミニチャレンジ 🔧

  1. 自作 JS util に JSDoc 型 → .d.ts 分離 を試し、“型エラー 0” が保てるか比較。
  2. Axios Response 型の追加メタデータ (cacheHit?:boolean) を module augmentation で拡張。
  3. DefinitelyTyped の PR ガイドに沿って @types/<your-pkg> をローカルで生成し、CI (npm test) を通す。

🚀 まとめ

  • 宣言ファイルは 「実装ゼロの型パッケージ」。JS 資産・外部ライブラリ・既存型の拡張を 安全に合体 できる。
  • コードベースでは types/ ディレクトリ + typeRoots、ライブラリでは "types" フィールド or DefinitelyTyped
  • Module Augmentation & Global Declaration を駆使し、React/MUI/Next.js の 型 DX を一段引き上げる
T-unityT-unity

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 候補 構造的型情報 で正確 & 重複ゼロ
リネームセーフ F2addUsercreateUser 使用箇所を静的解析、文字列・コメントは除外
ジャンプ / 参照検索 Go to Definition 型グラフですばやく解決
自動 import format() と書くだけで import { format } from "date-fns"; 型解決済みシンボル を優先候補
エラー抑制ヒント ?.! 提案 nullability 情報 をトラッキング

2️⃣ 実例:“any → 型安全” で DX がどう変わるか

2-1. before – any 地獄

// 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️⃣ すぐ試せるミニチャレンジ 🏃‍♀️

  1. rename vs grep ベンチ

    • iduserId を IDE Rename と VSCode 全文検索で速度比較し数値化。
  2. 型テストスナップショット

    npx tsd  # fail0? → commit gate
    
  3. PR テンプレに “tsc –noEmit success ✔️” チェックボックス を追加してみる。


🚀 まとめ

  • 型安全は DX を“体感スピード・安全・可読性”の三方向で底上げするレバレッジ。
  • IDE 機能・CI パイプライン・自動ドキュメント化が “書く前・動かす前” にバグを除去し、開発者の思考帯域をビジネスロジックに集中させる。
  • 赤波線ゼロ文化型テスト を組み合わせ、コードベースの成長とともに 精神的負債 を逆に減らす組織へ。