型システムの深掘り・実践設計・パフォーマンス最適化まで
コードで学ぶ。型で設計する。パフォーマンスで仕上げる。
この記事で得られること
- TypeScriptの型システムを「使う」から「設計に活かす」レベルへ引き上げる
- 実務で使える実践的なパターンと設計手法
- ビルド・パフォーマンスの最適化テクニック
対象読者:TypeScriptを書いたことはあるが、もっと深く使いこなしたい方
Part 1: 型システムの深掘り
1-1. 型推論の仕組みを理解する
TypeScriptの型推論は見た目以上に賢いです。まず「推論に任せる場所」と「明示すべき場所」を区別しましょう。
// ✅ 推論に任せてよい場所
const user = { id: "1", name: "Masaki" }; // { id: string; name: string }
const nums = [1, 2, 3]; // number[]
const double = (x: number) => x * 2; // (x: number) => number
// ✅ 明示すべき場所(推論が意図と違う、または関数の公開API)
function fetchUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
// ❌ 推論できるのに書いている(ノイズになる)
const name: string = "Masaki";
const count: number = 0;
widening(型の拡大)に注意する
// let は型が widening される
let status = "loading"; // string("loading"ではない!)
const status2 = "loading"; // "loading"(リテラル型)
// 意図的にリテラル型にしたい場合
let status3 = "loading" as const; // "loading"
satisfies 演算子で型チェックと推論を両立(TS 4.9+)
const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | number[]>;
// satisfies なし → string | number[]
// satisfies あり → 元の型を保持しつつ制約チェック
palette.red; // number[](string | number[] ではない)
palette.green; // string
1-2. Union型とNarrowing の完全攻略
4種類のNarrowing手法
type Value = string | number | null | { label: string };
function process(val: Value) {
// 1. typeof
if (typeof val === "string") {
console.log(val.toUpperCase()); // string
}
// 2. instanceof
if (val instanceof Date) {
console.log(val.toISOString()); // Date
}
// 3. in演算子
if (val !== null && "label" in val) {
console.log(val.label); // { label: string }
}
// 4. truthiness
if (val) {
// null / undefined / 0 / "" が除外される
}
}
User-Defined Type Guard でカスタムNarrowingを作る
interface Cat { meow(): void }
interface Dog { bark(): void }
type Animal = Cat | Dog;
// 型ガード関数
function isCat(animal: Animal): animal is Cat {
return "meow" in animal;
}
function makeSound(animal: Animal) {
if (isCat(animal)) {
animal.meow(); // Cat と確定
} else {
animal.bark(); // Dog と確定
}
}
Exhaustiveness Check で全ケース漏れを防ぐ
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// ここに到達したらコンパイルエラー(全ケース網羅を強制)
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${_exhaustive}`);
}
}
// 新しい Shape を追加したら、area() でコンパイルエラーが出るため漏れに気づける
1-3. Generics の実践的活用
制約付きGenerics
// K は T のキーに限定する
function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
return arr.map(item => item[key]);
}
const users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
const names = pluck(users, "name"); // string[]
const ages = pluck(users, "age"); // number[]
pluck(users, "email"); // ❌ コンパイルエラー
Default Type Parameters(TS 5.0+)
interface PaginatedResponse<T, Meta = { total: number; page: number }> {
data: T[];
meta: Meta;
}
type UserList = PaginatedResponse<User>; // Meta はデフォルトの型を使用
type CustomList = PaginatedResponse<Post, { cursor: string }>;
1-4. 高度な型操作
Conditional Types で型を計算する
// null / undefined を除去する
type NonNullable<T> = T extends null | undefined ? never : T;
// Promise を unwrap する
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
// 関数の引数の型を取り出す
type Parameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never;
// 実用例:APIレスポンスから型を生成
type ApiResponse<T> = T extends { data: infer D } ? D : never;
Mapped Types で型を変換する
// 全プロパティをDeepReadonlyにする
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// キーをremapする(TS 4.1+)
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 }
Template Literal Types でAPI型安全性を高める
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type ApiRoute = `/${ApiVersion}/${Resource}`;
// イベント名の型安全な定義
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
Part 2: 実践的なパターン・設計
2-1. Discriminated Union で状態設計を極める
フロントエンドでの非同期状態管理
// ❌ よくある壊れやすい実装
type State = {
isLoading: boolean;
data: User[] | null;
error: Error | null;
};
// isLoading: true && data: [...] という矛盾状態が作れる
// ✅ 状態を排他的に定義する
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
// React での使用例
function UserList() {
const [state, setState] = useState<AsyncState<User[]>>({ status: "idle" });
const fetchUsers = async () => {
setState({ status: "loading" });
try {
const data = await api.getUsers();
setState({ status: "success", data });
} catch (error) {
setState({ status: "error", error: error as Error });
}
};
switch (state.status) {
case "idle": return <button onClick={fetchUsers}>Load</button>;
case "loading": return <Spinner />;
case "success": return <List items={state.data} />; // data は確実に存在
case "error": return <ErrorView error={state.error} />;
}
}
2-2. Result型パターンで例外を型安全に扱う
// Result型の定義
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// ヘルパー関数
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
// 使用例
async function parseJSON<T>(text: string): Promise<Result<T, SyntaxError>> {
try {
return ok(JSON.parse(text) as T);
} catch (e) {
return err(e as SyntaxError);
}
}
// 呼び出し側で例外処理が強制される
const result = await parseJSON<User>(responseText);
if (result.ok) {
console.log(result.value.name); // User と確定
} else {
console.error(result.error.message); // SyntaxError と確定
}
2-3. Builder パターンで型安全なAPI設計
class QueryBuilder<T extends Record<string, unknown>> {
private filters: Partial<T> = {};
private _limit = 10;
private _offset = 0;
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters[key] = value;
return this;
}
limit(n: number): this {
this._limit = n;
return this;
}
offset(n: number): this {
this._offset = n;
return this;
}
async execute(): Promise<T[]> {
// DB クエリ実行
return fetch(`/api/query?${new URLSearchParams({
filters: JSON.stringify(this.filters),
limit: String(this._limit),
offset: String(this._offset),
})}`).then(r => r.json());
}
}
// 使用例
const users = await new QueryBuilder<User>()
.where("status", "active") // status は User のキー以外不可
.limit(20)
.offset(0)
.execute();
2-4. zodで実行時検証と型を一元管理する
import { z } from "zod";
// スキーマが型定義を兼ねる(二重管理を排除)
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(50),
email: z.string().email(),
role: z.enum(["admin", "member", "guest"]),
createdAt: z.coerce.date(), // 文字列を自動でDateに変換
});
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = UserSchema.partial().required({ id: true });
type User = z.infer<typeof UserSchema>;
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// APIハンドラーでの使用
async function createUser(req: Request) {
const body = await req.json();
const input = CreateUserSchema.safeParse(body);
if (!input.success) {
return Response.json(
{ errors: input.error.flatten() },
{ status: 400 }
);
}
// input.data は CreateUserInput 型が保証されている
const user = await db.users.create(input.data);
return Response.json(user);
}
2-5. Branded Types で同じ型の混同を防ぐ
// 同じ string でも意味が違う場合に混同を防ぐ
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { [__brand]: B };
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
// コンストラクタ関数
function UserId(id: string): UserId {
if (!id.match(/^user_/)) throw new Error("Invalid UserId");
return id as UserId;
}
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = UserId("user_123");
const postId = "post_456" as PostId;
getUser(userId); // ✅
getUser(postId); // ❌ コンパイルエラー!PostId を UserId に渡せない
Part 3: パフォーマンス・ビルド最適化
3-1. TypeScript コンパイル速度の最適化
tsconfig.json の最適設定
{
"compilerOptions": {
// 型チェックのみ(トランスパイルはesbuild/SWCに任せる)
"noEmit": true,
// インクリメンタルコンパイルで再ビルドを高速化
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
// 必要なlibだけ指定(不要な型定義の読み込みを避ける)
"lib": ["ES2022", "DOM"],
"target": "ES2022",
// プロジェクト参照で大規模プロジェクトを分割コンパイル
"composite": true,
// 厳格モードは必ず有効に
"strict": true,
// 型のみのimportを明示(バンドラー最適化に有効)
"verbatimModuleSyntax": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Project References で大規模プロジェクトを高速化
monorepo/
├── tsconfig.json # ルート(参照のみ)
├── packages/
│ ├── core/ tsconfig.json(composite: true)
│ ├── ui/ tsconfig.json(composite: true, references: [core])
│ └── app/ tsconfig.json(composite: true, references: [ui, core])
// packages/app/tsconfig.json
{
"compilerOptions": { "composite": true },
"references": [
{ "path": "../core" },
{ "path": "../ui" }
]
}
これにより変更があったパッケージのみ再コンパイルされます。
3-2. 型チェックと型定義のパフォーマンス
重い型は避ける
// ❌ 深いネストの条件型はコンパイルを遅くする
type DeepUnwrap<T> = T extends Promise<infer U>
? DeepUnwrap<U>
: T extends Array<infer U>
? DeepUnwrap<U>
: T;
// ✅ interface のマージは type より高速
interface Base { id: string }
interface Extended extends Base { name: string } // ✅ 速い
type ExtendedType = Base & { name: string }; // △ 毎回計算される
type-fest で型ユーティリティを再利用する
自前で複雑な型を実装する前に type-fest を確認しましょう。
import type { Except, Merge, RequireAtLeastOne, ReadonlyDeep } from "type-fest";
type UserWithoutPassword = Except<User, "password">;
type MergedConfig = Merge<DefaultConfig, UserConfig>;
// 少なくとも1つのプロパティが必要な型
type SearchParams = RequireAtLeastOne<{
name?: string;
email?: string;
id?: string;
}>;
3-3. バンドルサイズの最適化
type-only import で不要なコードを除去
// ❌ 型なのにランタイムコードに残る可能性がある
import { User } from "./types";
// ✅ type-only import は必ずバンドルから除去される
import type { User } from "./types";
// ✅ verbatimModuleSyntax を tsconfig で有効にすると強制される
const enum の罠
// ❌ const enum はトランスパイラ(esbuild等)と相性が悪い
const enum Direction {
Up,
Down,
}
// ✅ as const を使う(ランタイムオブジェクトが残るが安全)
const Direction = {
Up: 0,
Down: 1,
} as const;
type Direction = typeof Direction[keyof typeof Direction];
3-4. 実行時パフォーマンスと型設計の関係
オブジェクトの形状を統一してV8最適化を活かす
// ❌ 動的にプロパティを追加するとHidden Classが壊れる
function createUser(name: string) {
const user: any = {};
user.name = name;
user.id = Math.random(); // 後から追加
return user;
}
// ✅ 最初から完全な形状で作る(型でも強制される)
function createUser(name: string): User {
return {
id: crypto.randomUUID(),
name,
createdAt: new Date(),
};
}
Readonly を使って意図しないミューテーションを防ぐ
// 配列のReadonly
function sumArray(arr: readonly number[]): number {
// arr.push(1); // ❌ コンパイルエラー
return arr.reduce((a, b) => a + b, 0);
}
// 深いReadonly
type Config = Readonly<{
db: Readonly<{
host: string;
port: number;
}>;
}>;
まとめ:最強TSエンジニアのチェックリスト
型システム編
- [ ]
anyを使わずunknownか適切な型で代替できる - [ ] Discriminated Union で状態を排他的に設計できる
- [ ]
satisfies演算子を使いこなせる - [ ] Conditional Types・Mapped Types・Template Literal Types を書ける
- [ ] Exhaustiveness Check を実装できる
- [ ] Branded Types で意味的な型安全性を実現できる
設計パターン編
- [ ] Result型パターンで例外を型安全に扱える
- [ ] zod でスキーマと型を一元管理できる
- [ ] Builder パターンで型安全なAPIを設計できる
- [ ] 状態管理を Discriminated Union で設計できる
パフォーマンス編
- [ ]
incrementalビルドを設定している - [ ]
type-onlyimport を使い分けられる - [ ]
const enumの罠を知っている - [ ]
verbatimModuleSyntaxを有効にしている - [ ] 大規模プロジェクトで Project References を使える
参考リソース
| リソース | 内容 |
|---|---|
| TypeScript公式ドキュメント | 最も正確な一次情報 |
| TypeScript Playground | 型の動作を即確認 |
| Total TypeScript | Matt Pocock による実践的なTS学習 |
| type-fest | 実用的な型ユーティリティ集 |
| zod | スキーマ駆動の型定義 |
| TypeScript Performance Wiki | 公式パフォーマンスガイド |
最後まで読んでいただきありがとうございます。 質問やフィードバックはコメントやXにてお気軽にどうぞ!