「union型の共通プロパティ」とは何か
まず前提から整理します。
union型は「A か B かどちらか」の型でした。
type User = {
id: number;
name: string;
};
type Admin = {
id: number;
name: string;
permissions: string[];
};
type Person = User | Admin;
TypeScriptPerson は User か Admin のどちらかです。
ここで重要なのが 「共通しているプロパティ」と「片方にしかないプロパティ」 の違いです。
id と name は両方にある「共通プロパティ」。permissions は Admin にしかない「片方だけのプロパティ」。
TypeScript は、この違いをちゃんと理解していて、
共通プロパティ → そのまま安全にアクセスできる
片方だけのプロパティ → そのままでは危険なので、型ガードが必要
というルールで動きます。
ここから、具体例でこの感覚を固めていきます。
共通プロパティは「そのまま使える」
union型全体に“必ず存在する”ものは安全
さきほどの Person 型を使います。
type User = {
id: number;
name: string;
};
type Admin = {
id: number;
name: string;
permissions: string[];
};
type Person = User | Admin;
TypeScriptこのとき、次の関数はOKです。
function printBasicInfo(person: Person) {
console.log(person.id); // OK
console.log(person.name); // OK
}
TypeScriptなぜかというと、
User にも Admin にも id と name がある
→ Person のどちらのパターンでも、そのプロパティは必ず存在する
→ だから union 型のままでも安全にアクセスできる
からです。
ここでのポイントは、「TypeScript は union の“全メンバーに共通して存在するプロパティ”だけを、安全なものとして認める」ということです。
逆に言うと、「どのケースでも必ずある」ことが保証されているものだけ、何も考えずに使える、ということですね。
片方だけのプロパティは「そのまま使えない」
型ガードなしでアクセスすると怒られる
同じ Person を使って、permissions にアクセスしようとします。
function printPermissions(person: Person) {
console.log(person.permissions);
// ~~~~~~~~~~~~~~~~~
// Property 'permissions' does not exist on type 'User'.
}
TypeScriptエラーの意味を言い換えると、
「Person の中には User も含まれているけど、User には permissions はないよ」
「だから、Person としか分かっていない状態で permissions を触るのは危ない」
ということです。
つまり、「union 型のままでは、共通プロパティ以外には触らせない」
これが TypeScript の基本姿勢です。
共通プロパティと型ガードの関係
共通プロパティでできること/できないこと
共通プロパティは、そのまま使えます。
しかし「共通しているのは名前だけで、中身の型が違う」場合は注意が必要です。
type A = {
value: string;
};
type B = {
value: number;
};
type AB = A | B;
function printValue(ab: AB) {
// ab.value.toUpperCase(); // エラー
}
TypeScriptvalue というプロパティ名は共通していますが、
A では string
B では number
です。
このとき、ab.value の型は string | number になります。
だから、toUpperCase(string 専用メソッド)は直接は呼べません。
ここでやれるのは、
共通のメソッドだけ使う(toString など)
型ガードで中身の型を絞ってから使う
のどちらかです。
function printValue(ab: AB) {
if (typeof ab.value === "string") {
console.log(ab.value.toUpperCase()); // ここでは string
} else {
console.log(ab.value.toFixed(2)); // ここでは number
}
}
TypeScriptこの例から分かる大事なポイントは、
「共通プロパティ」というのは“名前が同じ”だけでなく、“型も共通”である必要がある、ということです。
共通プロパティをうまく設計すると、扱いやすいunion型になる
「本当に共通しているもの」を1つの型として切り出す
さきほどの User と Admin は、id と name が完全に共通していました。
こういう場合、「共通部分だけを別の型に切り出す」とコードがかなり読みやすくなります。
type BasePerson = {
id: number;
name: string;
};
type User = BasePerson & {
// 追加情報があればここに書く
};
type Admin = BasePerson & {
permissions: string[];
};
type Person = User | Admin;
TypeScriptこうしておくと、
「Person はみんな BasePerson の性質を持っている」
「Admin だけが extra として permissions を持っている」
という構造が、型の定義そのものから読み取れるようになります。
そうすると、
共通処理(名前やIDを表示するなど)は BasePerson 前提で書く
role ごとの処理は、型ガードで Admin / User に分けて書く
という整理されたコードを作りやすくなります。
共通プロパティを意識して型を設計すると、“絶対にあるもの”と“あるかないかが変わるもの”の境界が明確になる、というのが大きなメリットです。
実践的なイメージ:共通プロパティだけを見る関数/分岐で使い分ける
共通プロパティだけを扱う汎用関数
例えば、「誰であれ id と name さえあれば表示できる」関数は、共通プロパティ前提で書けます。
function printNameAndId(person: Person) {
console.log(person.id, person.name); // 共通プロパティだけ
}
TypeScriptこの関数は、User でも Admin でも、安全に呼べます。
「共通部分だけを観る関数」だからです。
特定の型だけが持つプロパティは、型ガードとセットで
一方で「権限を持つ人だけ特別扱いしたい」処理では、共通プロパティに頼れません。permissions は Admin にしかないからです。
function printIfAdmin(person: Person) {
if ("permissions" in person) {
// ここから先は Admin の世界
console.log(person.name, person.permissions);
} else {
console.log(person.name, "(一般ユーザー)");
}
}
TypeScriptこのように、
共通プロパティだけを見る関数
型ガードで分岐して、特定の型だけのプロパティを見る関数
を使い分けていくと、union 型の扱いがぐっと整理されます。
まとめ:union型の共通プロパティをどう捉えるか
最後に、感覚だけまとめます。
union 型では、「全てのパターンに必ず存在するプロパティ」だけが、
何の条件分岐もなしに安全に触れる領域になります。
それ以外のプロパティは、
共通でなかったり
型が違っていたり
特定のパターンだけに存在したり
するので、「まずは if / 型ガードで“今どのパターンか”を確かめてから触るもの」になります。
だから、型を設計するときはいつも、
この union に共通して必ず存在する情報は何か?
逆に、パターンごとに変わる情報はどれか?
を意識してみてください。
共通プロパティを見極めてあげると、
union 型は単なる「ごちゃっとした複数パターン」ではなく、
「共通部分+役割ごとの差分」という、きれいな構造を持った表現に変わっていきます。
