開発の現場には、不思議と「任せられる」人がいます。仕様が揺れていても、締切が迫っていても、彼らは慌てて断言しません。むしろこう言います。「いま確実に言えるのはここまで。残りは確認して、約束できる形にして持ち帰ります」。
型付けも同じです。全部わかったふりをする any
ではなく、「ここだけは必要です」と明確に言える最小限を宣言する。そうすれば、曖昧さは減り、実装は進み、余計な事故も起きません。
その“最小限の約束”を、TypeScript では 制約付きジェネリクスで表せます。
any
は便利だけど、約束がゼロ
// ❌ なんでも通るが、なんの保証もない
function doThing(value: any) {
// コンパイルは通るが、実行時に落ちるかもしれない
value.fullName.toUpperCase();
}
any
は「今はとりあえず答える」ための言い切りです。会議のその場しのぎと同じで、後から信用コストを払いがち。
「型を固定」もやり過ぎると、相手の情報を落とす
// ✅ だが狭すぎる: 返り値から余分な情報が消える
function echoNarrow(x: { fullName: string }) {
return x;
}
const obj = { fullName: "ok", extra: 123 } as const;
const r1 = echoNarrow(obj);
// r1 の型: { fullName: string }(extra が消えた)
会議で「この一点だけです」としかメモを取らないと、あとで“その他の文脈”が失われるのと同じ。
ちょうどよい中庸:T extends { fullName: string }
// ✅ 必要なものは保証しつつ、相手の情報は丸ごと保持
function echoWide<T extends { fullName: string }>(x: T): T {
return x;
}
const obj = { fullName: "ok", extra: 123 } as const;
const r2 = echoWide(obj);
// r2 の型: { readonly fullName: "ok"; readonly extra: 123 }
「このプロパティは必須です」だけを約束し、その他は相手のまま。
まさに、「不用意に断言しないが、必要な約束は持ち帰らない」という姿勢です。
基本パターン
function useFullName<T extends { fullName: string }>(x: T) {
const upper = x.fullName.toUpperCase(); // 安全
// もし呼び出し元が extra を持っていれば、ここでも候補に出る
// x.extra
}
useFullName({ fullName: "hi", extra: 42 }); // ✅
useFullName({ extra: 42 }); // ❌ Property 'fullName' is missing
“持ち帰って”付加情報を足すユーティリティ
function stamp<T extends { id: string }>(input: T) {
return { ...input, stampedAt: new Date() };
}
const payload = { id: "u1", role: "admin" };
const stamped = stamp(payload);
// { id: string; role: string; stampedAt: Date }
必要条件(id)だけ固める → あとは自然に広がる。
議事録の「確定事項」と同じ設計です。
Optional を許して、あとで確認する
「たぶんあるはず」な項目は Optional で受けて、使う前に“確認”します。
function readMaybe<T extends { fullName?: string }>(x: T) {
if (!x.fullName) throw new Error("Missing fullName");
return x.fullName.toUpperCase(); // 以降は安全に使える
}
型ガードで“確認済み”を型に反映する手もあります。
type WithFullName = { fullName: string };
function hasFullName<T extends { fullName?: unknown }>(x: T): x is T & WithFullName {
return typeof (x as any).fullName === "string";
}
function demo<T extends { fullName?: unknown }>(x: T) {
if (hasFullName(x)) {
x.fullName.toUpperCase(); // ここでは T & { fullName: string }
}
}
複数キーでも同じ発想で
function process<T extends { fullName: string; count: number }>(x: T) {
x.fullName.toUpperCase();
x.count.toFixed(0);
}
ユーティリティで汎用化もできます。
type With<K extends string, V> = Record<K, V>;
function byKey<K extends string, V, T extends With<K, V>>(x: T, key: K) {
return x[key]; // V として扱える
}
keyof
と組み合わせても、相手の情報を保つ
function pick<T extends { fullName: string }, K extends keyof T>(x: T, key: K) {
return x[key]; // T[K]
}
const obj = { fullName: "x", count: 3 };
const v1 = pick(obj, "fullName"); // string
const v2 = pick(obj, "count"); // number
すぐ使えるスニペット集
1) 「必須キーだけ保証して通す」
export function ensureFullName<T extends { fullName: string }>(x: T) {
// ここでは fullName が必ず使える
return x.fullName.trim();
}
2) 「入力を保ったまま 1 フィールド付加」
export function withTag<T extends { id: string }>(x: T, tag: string) {
return { ...x, tag };
}
3) 「Optional を受けてから確認」
export function must<T extends { fullName?: string }>(x: T) {
if (!x.fullName) throw new Error("fullName is required at runtime");
return x as T & { fullName: string };
}
4) 「配列から“必須が満たされているものだけ”を使う」
export function filterValid<T extends { fullName?: string }>(items: T[]) {
return items.filter((i): i is T & { fullName: string } => !!i.fullName);
}
まとめ
-
any
は「今は答えるけど、正確ではない」という無保証の言い切り。 -
T extends { fullName: X }
は必要最小限の約束だけを固め、相手の情報を丸ごと残す - 仕様が揺れる局面、ユーティリティ、ビルダー、アダプタでは、この“中庸”が最も強い
正確な interface をいま決めきれないなら── any
ではなく extends
を。
約束できる最小限を明確にし、残りは誠実に“持ち帰る”。それが、型と実装とチームを前に進めます。