「型を書く」から「型で設計する」へ。初心者が本物のTSエンジニアになるための地図。


はじめに

TypeScriptを「JavaScriptに型をつけたもの」だと思っていませんか?

それは半分正解で、半分大きな誤解です。

TypeScriptの真の力は 「コンパイル時に間違いを発見する」 ことではなく、「型を使ってシステム全体の設計を表現する」 ことにあります。

このロードマップでは、初心者が現場で通用する最強のTSエンジニアになるための道筋を、4つのステージに分けて解説します。


Stage 1 ― 型の基礎を「本当に」理解する

1-1. プリミティブ型と型推論

多くの初心者がここで躓く「型を書きすぎる」問題から始めましょう。

// ❌ 悪い例:型を全部書こうとする
const name: string = "Masaki";
const age: number = 30;
const isActive: boolean = true;

// ✅ 良い例:TypeScriptに推論させる
const name = "Masaki";   // string と推論される
const age = 30;          // number と推論される
const isActive = true;   // boolean と推論される

型推論を信頼するのが最強TSエンジニアへの第一歩です。型アノテーションは「推論できない場所」にだけ書きます。


1-2. any は麻薬である

// ❌ any を使うとTSの恩恵がゼロになる
function processData(data: any) {
  return data.someMethod(); // 実行時エラーが起きても気づけない
}

// ✅ unknown を使う(型チェックを強制できる)
function processData(data: unknown) {
  if (typeof data === "string") {
    return data.toUpperCase(); // ここでは string と確定している
  }
  throw new Error("Unexpected data type");
}

any を使ってよいのは「型定義のない古いライブラリを暫定的につなぐ時」だけです。それ以外は unknown か、適切な型を定義してください。


1-3. Union型とNarrowingをマスターする

type Status = "loading" | "success" | "error";

function renderUI(status: Status) {
  // TypeScriptが網羅チェックをしてくれる
  switch (status) {
    case "loading":
      return <Spinner />;
    case "success":
      return <Content />;
    case "error":
      return <ErrorMessage />;
    // default不要:全ケース網羅でコンパイルエラーにならない
  }
}

Stage 1 のゴール:型推論・Union型・Narrowingを使い、any なしでコードが書ける。


Stage 2 ― 型システムを「設計ツール」として使う

2-1. interfaceとtypeの使い分け

// interface:オブジェクトの形を定義する(拡張前提)
interface User {
  id: string;
  name: string;
}

interface AdminUser extends User {
  permissions: string[];
}

// type:Union・交差型など複雑な型を定義する
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

// 使い方
function fetchUser(id: string): Result<User> {
  // ...
}

迷ったら:オブジェクト構造には interface、それ以外の複合型には type を使うのが現場のコンセンサスです。


2-2. Generics(ジェネリクス)で再利用可能な型を作る

ジェネリクスはTypeScriptで「型レベルの関数」です。

// ❌ 型ごとに同じ関数を書く
function getFirstString(arr: string[]): string | undefined {
  return arr[0];
}
function getFirstNumber(arr: number[]): number | undefined {
  return arr[0];
}

// ✅ ジェネリクスで1つにまとめる
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

const first = getFirst([1, 2, 3]);    // T = number と推論
const name = getFirst(["a", "b"]);    // T = string と推論

2-3. Utility Typesを使いこなす

TypeScriptには便利な組み込み型が多数あります。これを知らずに自分で実装するのは時間の無駄です。

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

// Partial:全プロパティをオプショナルに
type UpdateUserInput = Partial<User>;

// Pick:必要なプロパティだけ取り出す
type UserPreview = Pick<User, "id" | "name">;

// Omit:特定のプロパティを除外する
type PublicUser = Omit<User, "password">;

// Required:全プロパティを必須に
type StrictUser = Required<User>;

// Readonly:変更不可にする
type ImmutableUser = Readonly<User>;

// Record:keyとvalueの型を指定したオブジェクト
type UserMap = Record<string, User>;

Stage 2 のゴール:Generics・Utility Typesを使い、DRYな型定義が書ける。


Stage 3 ― 型で「バグを不可能にする」設計をする

ここからが本物のTSエンジニアの領域です。

3-1. Discriminated Union で状態管理を安全にする

// ❌ 悪い例:フラグが増えると破綻する
type FetchState = {
  isLoading: boolean;
  data: User | null;
  error: string | null;
};
// isLoading: true かつ data: User という矛盾した状態が作れてしまう

// ✅ 良い例:Discriminated Union で状態を排他的にする
type FetchState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };

function render(state: FetchState) {
  switch (state.status) {
    case "idle":
      return null;
    case "loading":
      return <Spinner />;
    case "success":
      return <UserCard user={state.data} />; // data が必ず存在する
    case "error":
      return <ErrorMessage message={state.error} />; // error が必ず存在する
  }
}

これで「ローディング中にデータが存在する」という不正な状態を型レベルで表現不可能にできます


3-2. Template Literal Types でAPIルートを型安全にする

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";

type ApiRoute = `/${ApiVersion}/${Resource}`;
// => "/v1/users" | "/v1/posts" | "/v1/comments" | "/v2/users" | ...

function apiCall(method: HttpMethod, route: ApiRoute) {
  // route は有効なAPIパスのみ受け付ける
}

apiCall("GET", "/v1/users");   // ✅ OK
apiCall("GET", "/v3/users");   // ❌ コンパイルエラー! /v3 は存在しない

3-3. satisfies 演算子で型チェックと推論を両立する(TS 4.9+)

const config = {
  theme: "dark",
  language: "ja",
  maxRetries: 3,
} satisfies Record<string, string | number>;

// satisfies を使うと:
// 1. Record<string, string | number> の制約でチェックされる
// 2. 推論は元の型を保持(config.theme は string ではなく "dark" のまま)

config.theme; // 型は "dark"(satisfies なしなら string になってしまう)

Stage 3 のゴール:型で不正な状態を表現不可能にする設計ができる。


Stage 4 ― 型レベルプログラミングで「型を計算する」

最強の領域です。ここまで来ると型パズルが楽しくなります。

4-1. Conditional Types

// T が配列なら要素の型を返す、そうでなければ T をそのまま返す
type Unwrap<T> = T extends Array<infer Item> ? Item : T;

type A = Unwrap<string[]>;  // string
type B = Unwrap<number>;    // number
type C = Unwrap<User[]>;    // User

4-2. Mapped Types でオブジェクト型を変換する

// 全プロパティを関数に変換する
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// => { getName: () => string; getAge: () => number }

4-3. infer で型を「分解」する

// 関数の戻り値の型を取り出す(組み込みReturnTypeの自作)
type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

async function fetchUser(): Promise<User> {
  // ...
}

type FetchResult = MyReturnType<typeof fetchUser>; // Promise<User>
type Awaited = Awaited<FetchResult>;               // User

4-4. 実践:zodで実行時型検証を型と連携させる

import { z } from "zod";

// zodスキーマが型定義を兼ねる
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
});

// スキーマから型を自動生成
type User = z.infer<typeof UserSchema>;

// APIレスポンスの検証
async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return UserSchema.parse(data); // 失敗すれば例外、成功すれば型安全
}

Stage 4 のゴール:型を計算・変換し、zodなど外部ライブラリと連携した堅牢な型設計ができる。


まとめ:最強TSエンジニアの思考フレームワーク

ステージ指標キーワード
Stage 1any なしで書ける型推論・Union・Narrowing
Stage 2DRYな型が書けるGenerics・Utility Types
Stage 3不正状態を型で封じるDiscriminated Union・satisfies
Stage 4型を計算・変換できるConditional Types・infer・zod

最強のTSエンジニアとは**「型によってバグを発生不可能にする設計ができる人」**です。

ぜひこのロードマップを参考に、一歩ずつ型の力を身につけてください。


参考リソース


この記事が参考になったら、いいね・ストック・フォローをお願いします!