TypeScript基礎講座 #7 ユーティリティ型とtsconfig

読了 11分

前回はジェネリクスの深い道具を扱いました。今回がシリーズ最後の記事です。二つを整理して締めくくります — 実戦で毎日使うことになる標準ユーティリティ型、そしてコンパイル動作を決めるtsconfig.jsonの主要オプションです。

ユーティリティ型とは #

TypeScriptには、よく使う型変換パターンがビルトインのユーティリティ型として事前に定義されています。importなしで、どこからでもすぐに使えます。

ユーティリティ型の例
type User = { id: string; name: string; age: number; email: string };

type UserPreview = Pick<User, 'id' | 'name'>;
//   = { id: string; name: string }

type UserUpdate = Partial<User>;
//   = { id?: string; name?: string; age?: number; email?: string }

これらの変換は#6で見たマップ型やインデックスアクセスのような道具で作られています — TypeScript自体で表現可能なんです。自分でも作れますが、よく使うものは標準ライブラリにあるので、それを使えば良いです。

代表的なものをカテゴリ別に見ていきましょう。

オブジェクト変形 — Partial / Required / Readonly #

Partial<T> — すべてのプロパティをオプショナルに #

Partial
type User = { id: string; name: string; age: number };
type UserPatch = Partial<User>;
// = { id?: string; name?: string; age?: number }

更新関数の引数の型によく使われます。

patch関数
function updateUser(id: string, patch: Partial<User>): void {
  // idでユーザーを探し、patchのフィールドだけ上書き
}

updateUser('u-1', { name: 'ヨンヒ' });        // ✓
updateUser('u-1', { name: 'ヨンヒ', age: 28 }); // ✓

Required<T> — すべてのオプショナルを必須に #

Partialの反対。?が付いたプロパティが必須になります。

Required
type Config = { host?: string; port?: number; timeout?: number };
type CompleteConfig = Required<Config>;
// = { host: string; port: number; timeout: number }

Readonly<T> — すべてのプロパティをreadonlyに #

Readonly
type ImmutableUser = Readonly<User>;

const u: ImmutableUser = { id: 'u-1', name: 'チョルス', age: 30 };
u.name = 'ヨンヒ';   // 🚫

キーの選択 — Pick / Omit / Record #

Pick<T, K> — 一部のキーだけ取り出す #

Pick
type User = { id: string; name: string; age: number; email: string };
type UserPreview = Pick<User, 'id' | 'name'>;
// = { id: string; name: string }

KはTのキーunionです。そこから選んだプロパティだけで新しい型を作ります。

Omit<T, K> — 一部のキーを除外 #

Omit
type UserWithoutEmail = Omit<User, 'email'>;
// = { id: string; name: string; age: number }

type UserWithoutEmailAndAge = Omit<User, 'email' | 'age'>;
// = { id: string; name: string }

複数のキーを除外するときはunionでまとめて渡します。PickとOmitはほぼ鏡像です。「必要なものを選ぶ」のはPick、「不要なものを除く」のはOmit。

Record<K, V> — キー-値マッピングオブジェクト #

Record
type Scores = Record<string, number>;
// = { [key: string]: number }

const scores: Scores = {
  math: 95,
  english: 87,
  science: 91,
};

特定のキーunionで制限することもできます。

Record with literal keys
type Roles = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Roles, string[]>;

const perms: Permissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read'],
};
// 三つのうち一つでも漏らすとコンパイルエラー

このパターンはenum-likeなデータにマッピングされる情報を安全に表現するときに強力です。

Union変形 — Exclude / Extract / NonNullable #

Exclude<T, U> — unionから一部の型を除去 #

Exclude
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = Exclude<AllColors, 'green' | 'blue'>;
// = 'red' | 'yellow'

TのunionからUに該当する型を除きます。

Extract<T, U> — unionから一部の型のみ抽出 #

Extract
type Mixed = string | number | boolean | null | undefined;
type Truthy = Extract<Mixed, string | number | boolean>;
// = string | number | boolean

Excludeと鏡像。

NonNullable<T> — null/undefinedを除去 #

NonNullable
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// = User

T | null | undefinedをよく扱うようになると、非常によく使うことになるユーティリティです。

関数関連 — ReturnType / Parameters / Awaited #

ReturnType<F> — 関数の戻り値の型 #

ReturnType
function fetchUser() {
  return { id: 'u-1', name: 'チョルス' };
}

type User = ReturnType<typeof fetchUser>;
// = { id: string; name: string }

#6でちらっと見たパターンです。関数が返すオブジェクトの形を別途型として定義せず、自動的に取得します。

Parameters<F> — 関数の仮引数の型(タプルとして) #

Parameters
function login(username: string, password: string): boolean {
  return true;
}

type LoginArgs = Parameters<typeof login>;
// = [username: string, password: string]

Awaited<T> — Promiseを解いた型 #

Awaited
type UserPromise = Promise<User>;
type UnwrappedUser = Awaited<UserPromise>;
// = User

// ネストされたPromiseも解く
type Nested = Promise<Promise<User>>;
type Unwrapped = Awaited<Nested>;
// = User (ネストまで解く)

async関数の戻り値の型処理によく使われます。

総合例 — 実戦的なパターン #

これまで見たユーティリティが一箇所に集まる小さな例。

実戦パターン
type User = {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
};

// クライアントに公開 — passwordを除去
type PublicUser = Omit<User, 'password'>;

// 登録フォームで受け取るデータ — idとcreatedAtはサーバーが決定
type SignupInput = Omit<User, 'id' | 'createdAt'>;

// プロフィール更新 — idを除いてすべてオプショナル
type ProfileUpdate = Partial<Omit<User, 'id'>>;

// ユーザー一覧カード — 一部だけ必要
type UserCard = Pick<User, 'id' | 'name'>;

// 権限マッピング
type Role = 'admin' | 'editor' | 'viewer';
type RolePermissions = Record<Role, string[]>;

// API関数シグネチャから仮引数を抽出
async function getUser(id: string): Promise<PublicUser> {
  // ...
  return {} as PublicUser;
}

type GetUserArgs = Parameters<typeof getUser>;     // [id: string]
type GetUserResult = Awaited<ReturnType<typeof getUser>>;  // PublicUser

同じUserデータの形から、状況別に必要な変形をすべて導き出します。データの形は一箇所に定義し、そこからさまざまなビューを派生させるんですね。データと型の同期コストが消え、Userの定義が変われば派生する型も自動で追従して更新されます。

tsconfig.json — コンパイラ設定 #

ここから二つ目のテーマに移ります。tsconfig.jsonはTypeScriptコンパイラのすべての動作を決める設定ファイルです。

npx tsc --initで作られるデフォルト設定には詳しいコメントが付いています。一度開いてみるとオプションが100個以上見えるはずです。幸い、日常的に気にするのはそのうち十数個だけです。

主要オプション — strict #

tsconfig.json (一部)
{
  "compilerOptions": {
    "strict": true
  }
}

strict: trueは複数の安全性オプションを一度にオンにするマスタースイッチです。

  • noImplicitAny — 推論失敗時の暗黙的anyを禁止
  • strictNullChecksnullundefinedを他の型と明確に区別
  • strictFunctionTypes — 関数の仮引数の型互換性を厳密に
  • strictBindCallApplybind/call/applyの引数チェック
  • strictPropertyInitialization — クラスプロパティの初期化を強制
  • noImplicitThis — 暗黙的anyのthisを禁止

新規プロジェクトなら常にstrict: trueから始めてください。 このオプションを切るとTypeScriptの最も大きな価値が半分以上失われます。既存JavaScriptプロジェクトをマイグレーションする場合は段階的にオンにすることもありますが、新規プロジェクトでstrictを切るのはほぼ常に間違った選択です。

target — どのJavaScriptに変換するか #

{ "compilerOptions": { "target": "ES2022" } }

TypeScriptをどのバージョンのJavaScriptに変換するかを決めます。

  • ES5 — 古いIEまでサポートが必要なとき(まれになっています)
  • ES2017~ES2022 — モダンブラウザ
  • ESNext — 最新

ブラウザ対象なら通常ES2020以上、Node.js最新バージョン対象ならES2022程度が無難です。

module — モジュールシステム #

{ "compilerOptions": { "module": "ESNext" } }

import/exportコードをどう変換するかを決めます。

  • CommonJS — Node.js伝統の方式(require)
  • ESNext / ES2022 — 標準ESモジュール
  • NodeNext — Node.jsのESモジュール + CommonJS混在処理

バンドラ(Vite、Webpack)を使うならほぼ常にESNext。純粋なNode.jsプロジェクトならNodeNextが通常の答えです。

moduleResolution — モジュールの探索方式 #

{ "compilerOptions": { "moduleResolution": "bundler" } }

TypeScriptがimportパスを解釈する方式。最近ではbundler(Vite/Webpackなどバンドラのための新しいモード)やNodeNextが推奨されます。

lib — 利用可能なビルトインAPI #

{ "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"] } }

TypeScriptが認識するビルトインライブラリの型です。ブラウザで動作するならDOMを含め、Node.js専用なら除いて@types/nodeを使用します。

outDir / rootDir — 入出力パス #

シンプルな設定
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist"
  }
}
  • rootDir — ソースコードのルート(どこからコンパイルを開始するか)
  • outDir — 出力ファイルの場所

outDirを指定するとコンパイルされた.jsがそのフォルダに集まります。指定しないと.tsファイルと同じ場所に生成されてフォルダが散らかってしまいます。

include / exclude — どのファイルをコンパイルするか #

対象ファイルの指定
{
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

includeでコンパイル対象を指定し、excludeで除外するフォルダを並べます。node_modulesはほぼ常にexcludeに含まれています。

sourceMap — デバッグ用ソースマップ #

{ "compilerOptions": { "sourceMap": true } }

.js.mapファイルを追加で生成し、ブラウザ/Nodeデバッガで元の.tsファイルとマッピングされるようにします。デバッグがずっと楽になります。

jsx — Reactプロジェクトの場合 #

Reactプロジェクト
{ "compilerOptions": { "jsx": "react-jsx" } }

JSXをどう変換するかを決めます。モダンReactならreact-jsx、Next.jsならpreserve(Next.jsが直接処理)。

総合 — モダン推奨設定 #

新規プロジェクトなら、次のあたりが基本的に良い出発点です。

tsconfig.json (モダン推奨)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",

    "strict": true,
    "noUncheckedIndexedAccess": true,

    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "declaration": true,
    "noEmit": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

追加で知っておくと良いオプション:

  • noUncheckedIndexedAccess — 配列インデックスアクセス結果に| undefinedを追加。arr[0]TではなくT | undefinedになります。安全性が大いに↑、たまに面倒。余裕があるならオンにすることを推奨
  • esModuleInterop — CommonJSとESモジュールの互換性向上。ほぼ常にtrue
  • skipLibCheck — 外部ライブラリの.d.tsチェックをスキップ。コンパイル速度↑。ほぼ常にtrue
  • declaration.d.tsファイルも一緒に生成。ライブラリを作るときに必要
  • noEmit — trueならコンパイル結果を出力しません。型チェック専用に使用(Viteのような他のツールがトランスパイルする場合)

Viteで作ったプロジェクトはtscがトランスパイルせず型チェックだけするので、noEmit: trueに設定されているはずです。

シリーズ振り返り #

このシリーズで私たちは次を扱いました。

#テーマ要点
1始まりとセットアップTS motivation、コンパイルの流れ、最初のコード
2基本の型string/number/boolean/array/tuple/object/enum、any/unknown
3interfaceとtype aliasオブジェクト型エイリアス、二つの道具の違い
4union/literal/narrowing複数の可能性の表現 + 型の絞り込み
5関数の型オプショナル/デフォルト/rest、オーバーロード、ジェネリクス入門
6ジェネリクス深掘り制約、keyof、インデックスアクセス、conditional types
7ユーティリティ型 + tsconfig標準ユーティリティ、コンパイル設定

TypeScriptの学習は一度にすべてを吸収するのではなく、使いながらだんだん慣れていく領域です。このシリーズでほぼすべての道具の名前と形に出会い、これからはコードを書いていて詰まったところで「あ、#4で見たあのnarrowingパターンが必要だな」と思い出せるようになっているはずです。

次のステップ #

すぐに挑戦できそうなこと #

  • 自分の小さなJavaScriptプロジェクトをTSに移行.js.tsにリネームしてエラーを一つずつ潰す
  • 新規プロジェクトは最初からTSで — ViteでもNext.jsでもTSオプションを選択
  • ライブラリの.d.tsファイルを一度読んでみるnode_modules/lodash/index.d.tsのようなところを開くと、実戦的な型のお手本が見える

すぐに必要になる領域 #

  • @types/...パッケージ — 型のないライブラリに型を追加
  • ZodまたはValibot — ランタイム検証ライブラリ。TSと組み合わせて安全性↑
  • TypeScript ESLint — Lintルール(例: no-explicit-anyprefer-const)

続編シリーズ — React + TypeScript #

たった今終えたReact 31編はすべてJavaScriptで書かれていますが、次はReact + TypeScriptシリーズを通して、それらすべてのパターンをTSの中でどう表現するかを扱う予定です。propsの型付け、フックの型付け、ジェネリックコンポーネント、ポリモーフィズム — 実務で最もよく使うパターンです。

まとめ #

ここまで7編シリーズを追いかけてくださってありがとうございます。TypeScriptは初めて出会うと急な学習曲線がありますが、ある臨界点を越えるとJavaScriptに戻れないくらい快適になる道具です。自動補完 / リファクタリングの安全性 / 即時のフィードバックの価値は、一度経験すると忘れがたいものです。

記憶に残すべきものを一つ挙げるなら — 「自分のデータの形を明示する習慣」です。関数シグネチャ、オブジェクト型、APIレスポンスの形をコードの上の方に明示的に書く習慣自体が、良いコードを書かせます。型は結局、コードについてより深く考えさせる道具です。

自分で作りたい小さなプロジェクトに戻って、今度はTypeScriptで始めてみてください。詰まるところで本当の学習が起こります。

X