strict で型検査を通したコードです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)。
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[]
}
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[]
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 などのスキーマ検証ライブラリを検討してください。
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 だと具体的な型が失われ、port は number になります。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,
};
}
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">") as SafeHtml;
}
function render(html: SafeHtml) { /* ... */ }
// エスケープを通していない文字列は render に渡せない
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)。
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)。