A2. 型レシピ集

コピーして使う型定義の断片 / 1レシピ20行以内

📅 作成: 2026-08-29 / 更新: 2026-08-29

この付録の使い方

レシピ一覧

  1. Result 型 — 失敗を戻り値で返す
  2. 型ガード関数 — 外部データを確かめる
  3. 設定オブジェクト型
  4. ブランド型 — 同じ形を区別する
  5. 部分更新と読み取り専用

A2.1 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 });

使い方

function parsePort(s: string): Result<number> {
	const n = Number(s);
	if (!Number.isInteger(n)) return err(new Error(`整数ではありません: ${s}`));
	if (n < 1 || n > 65535) return err(new Error(`範囲外です: ${n}`));
	return ok(n);
}

const r = parsePort("3000");
if (r.ok) {
	console.log(r.value);       // number
} else {
	console.error(r.error.message);
}

使いどころ: 失敗が想定内で、呼ぶ側に対処を強制したいとき。外部入力の検証ライブラリの境界に向きます。

落とし穴: すべての関数に使うと分岐だらけになります。境界で使い、内側は例外が実務的です(11.4)。

エラーの種類を型で区別する

type LoadError =
	| { kind: "not-found"; path: string }
	| { kind: "invalid-json"; path: string }
	| { kind: "invalid-shape"; index: number };

function load(path: string): Result<string[], LoadError> { /* ... */ }

const r = load("data.json");
if (!r.ok) {
	switch (r.error.kind) {
		case "not-found":     console.error(`ファイルがありません: ${r.error.path}`); break;
		case "invalid-json":  console.error(`JSON が壊れています: ${r.error.path}`); break;
		case "invalid-shape": console.error(`${r.error.index} 件目が不正です`); break;
	}
}

エラーも判別可能ユニオンにすると、対処の網羅性を検査できます(08.5)。

A2.2 型ガード関数 — 外部データを確かめる

オブジェクトの形を確かめる

type User = { name: string; age: number };

function isUser(v: unknown): v is User {
	if (typeof v !== "object" || v === null) return false;
	const o = v as Record<string, unknown>;
	return typeof o.name === "string" && typeof o.age === "number";
}

配列を確かめる

function isArrayOf<T>(v: unknown, isItem: (x: unknown) => x is T): v is T[] {
	return Array.isArray(v) && v.every(isItem);
}

const data: unknown = JSON.parse(text);
if (isArrayOf(data, isUser)) {
	console.log(data.length);     // User[]
}

undefined を除く(filter で型を絞る)

function isDefined<T>(v: T | undefined | null): v is T {
	return v !== undefined && v !== null;
}

const values: (string | undefined)[] = ["a", undefined, "b"];
const filtered = values.filter(isDefined);      // string[]

リテラルの union を確かめる

const STATUSES = ["pending", "active", "closed"] as const;
type Status = (typeof STATUSES)[number];        // "pending" | "active" | "closed"

function isStatus(v: unknown): v is Status {
	return typeof v === "string" && (STATUSES as readonly string[]).includes(v);
}

(typeof ARRAY)[number] は覚えておくと便利です。

配列の値から union 型を作れるため、値の一覧と型の一覧が二重管理になりません。要素を足せば型も自動で増えます。

落とし穴: 型ガードの中身は検査されません08.4)。return true; と書いても通ります。プロパティが増えたらガードも直す必要があります。数が増えたら zod などのスキーマ検証ライブラリを検討してください。

A2.3 設定オブジェクト型

satisfies で型を確かめつつ具体的な型を残す

type Config = {
	host: string;
	port: number;
	features: readonly string[];
};

export const CONFIG = {
	host: "localhost",
	port: 3000,
	features: ["auth", "logging"],
} as const satisfies Config;

// CONFIG.port の型は 3000(number ではない)
// CONFIG.features[0] の型は "auth"

as Config ではなく satisfies Config を使ってください。

as だと具体的な型が失われ、portnumber になります。satisfies なら検査もされるし、リテラル型も残ります06.3)。

省略可能な項目に既定値を与える

type Options = {
	host?: string;
	port?: number;
	timeout?: number;
};

// 内部で使う「すべて埋まった」形
type ResolvedOptions = Required<Options>;

const DEFAULTS: ResolvedOptions = {
	host: "localhost",
	port: 3000,
	timeout: 5000,
};

function resolve(opts: Options = {}): ResolvedOptions {
	return { ...DEFAULTS, ...opts };
}

{ ...DEFAULTS, ...opts } には落とし穴があります。

opts.port明示的に undefined だと、既定値を上書きして undefined になります。厳密にやるなら1項目ずつ ?? で埋めてください。

function resolve(opts: Options = {}): ResolvedOptions {
	return {
		host: opts.host ?? DEFAULTS.host,
		port: opts.port ?? DEFAULTS.port,
		timeout: opts.timeout ?? DEFAULTS.timeout,
	};
}

A2.4 ブランド型 — 同じ形を区別する

declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Positive = Brand<number, "Positive">;

生成関数とセットにする

function toUserId(s: string): UserId {
	if (!s.startsWith("user-")) throw new Error(`UserId の形式が不正です: ${s}`);
	return s as UserId;
}

function toPositive(n: number): Positive {
	if (!(n > 0)) throw new Error(`正の数ではありません: ${n}`);
	return n as Positive;
}

function getUser(id: UserId) { /* ... */ }

getUser(toUserId("user-001"));      // OK
getUser("user-001");                // エラー TS2345(as を通していない)

使いどころ: 取り違えると実害が出るもの。ID の種類・検証済みかどうか・単位の違う数値

落とし穴: すべての string に付けると変換だらけになります。実行時のコストはゼロですが、読みやすさのコストはあります(05.4)。

検証済みを型で表す

type RawInput = Brand<string, "RawInput">;
type SafeHtml = Brand<string, "SafeHtml">;

function escapeHtml(s: RawInput): SafeHtml {
	return s
		.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;") as SafeHtml;
}

function render(html: SafeHtml) { /* ... */ }
// エスケープを通していない文字列は render に渡せない

A2.5 部分更新と読み取り専用

更新用の型

type User = { id: string; name: string; age: number };

// id 以外を省略可能にした更新用の型
type UserUpdate = Partial<Omit<User, "id">>;

function update(id: string, patch: UserUpdate): void { /* ... */ }

update("1", { name: "Bob" });          // OK
update("1", { id: "2" });              // エラー(id は変更できない)

再帰的に読み取り専用にする

type DeepReadonly<T> = {
	readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

type Config = { db: { host: string; port: number } };
const c: DeepReadonly<Config> = { db: { host: "localhost", port: 5432 } };

c.db.port = 1;
//   ~~~~
// error TS2540: Cannot assign to 'port' because it is a read-only property.

Readonly<T>1段階しか効きません。入れ子まで守りたいときはこの形を使います(A4.2)。

少なくとも1つは必須

type AtLeastOne<T> = {
	[K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>;
}[keyof T];

type Filter = AtLeastOne<{ name: string; email: string; age: number }>;

const f1: Filter = { name: "Alice" };            // OK
const f2: Filter = { name: "A", age: 30 };       // OK
const f3: Filter = {};                           // エラー(何も指定していない)

この形は読みにくいので、使う前に検討してください。

実行時の検証(「1つ以上指定してください」というエラー)で十分な場面が多くあります。型で禁止する価値があるかを判断してから使ってください。

キーと値を対応させる

type EventMap = {
	click: { x: number; y: number };
	key: { code: string };
	close: undefined;
};

function emit<K extends keyof EventMap>(type: K, payload: EventMap[K]): void {
	console.log(type, payload);
}

emit("click", { x: 1, y: 2 });      // OK
emit("key", { x: 1, y: 2 });        // エラー(key の payload は { code: string })

イベント名ごとにデータの形が違う場合に有効です(09.2)。