ゴール:「クラスのプロパティに“どんな型をつけるか”を意識して設計できるようになること」
クラスを書くとき、
「プロパティ名」だけでなく「プロパティの型」をどう設計するかで、
そのクラスの使いやすさ・安全さがかなり変わります。
ここでは、
「とりあえず string / number を付ける」から一歩進んで、
- どこまで細かく型を決めるか
?やreadonlyをどう使うか- クラス外の型定義とどう分担するか
を、初心者向けにかみ砕いて整理していきます。
基本:まずは「素直な型」からスタートする
プロパティに型をつける一番シンプルな形
まずは、いちばん素直な例から。
class User {
id: number;
name: string;
email: string;
}
const u = new User();
u.id = 1;
u.name = "Taro";
u.email = "taro@example.com";
TypeScriptここでやっていることはシンプルで、
idは数値nameは文字列emailも文字列
という「そのままの型」を付けています。
最初のうちは、
「このプロパティにはどんな値が入る?」を素直に言葉にして、
それをそのまま型にする、で十分です。
コンストラクタと組み合わせた型設計
コンストラクタで初期化する場合も、型は同じです。
class User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
}
TypeScriptあるいは、TypeScript の省略記法を使うとこう書けます。
class User {
constructor(
public id: number,
public name: string,
public email: string
) {}
}
TypeScriptどちらの場合も、
「プロパティの型はコンストラクタ引数の型と一致させる」
というのが基本の考え方です。
オプショナル(?)と「null / undefined」をどう扱うか
「あってもなくてもいい」プロパティ:? を使う
たとえば、ユーザーのプロフィールで「自己紹介文」は任意だとします。
class User {
constructor(
public id: number,
public name: string,
public bio?: string
) {}
}
const u1 = new User(1, "Taro");
const u2 = new User(2, "Hanako", "こんにちは!");
TypeScriptbio?: string は、
「bio プロパティ自体が存在しないこともある」
という意味です(型としては string | undefined に近いイメージ)。
コード上では、こう扱います。
if (u1.bio) {
console.log(u1.bio.toUpperCase());
} else {
console.log("自己紹介は未設定");
}
TypeScript「必ずあるけど、値がないこともある」場合:string | null など
一方で、「プロパティは必ず存在するけど、中身がないこともある」
という設計もあります。
class User {
constructor(
public id: number,
public name: string,
public bio: string | null
) {}
}
const u1 = new User(1, "Taro", null);
const u2 = new User(2, "Hanako", "こんにちは!");
TypeScriptこの場合、bio プロパティは必ずありますが、
値として null が入る可能性があります。
どちらを選ぶかは「意味」の問題です。
- 「そもそもプロパティが存在しない」ことを表したい →
?(オプショナル) - 「プロパティはあるが、値がまだない/消された」ことを表したい →
nullを使う
ここを意識して選べるようになると、
プロパティの型設計が一段レベルアップします。
readonly:変えていいプロパティと、変えちゃダメなプロパティ
「一度決めたら変えない」ものは readonly にする
たとえば、ユーザーの id は一度決まったら変えない、
というルールにしたいとします。
class User {
readonly id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
const u = new User(1, "Taro");
u.name = "Jiro"; // OK
// u.id = 2; // エラー:readonly なので再代入できない
TypeScriptreadonly を付けると、
「コンストラクタの中では代入できるけど、それ以外では変更できない」
というルールになります。
これは「ビジネスルールを型で表現する」強力な手段です。
- ID
- 作成日時
- 生成時に決まって以降変わらないフラグ
などは、積極的に readonly を検討してみてください。
コンストラクタ省略記法+readonly
省略記法と組み合わせると、こう書けます。
class User {
constructor(
public readonly id: number,
public name: string
) {}
}
TypeScriptpublic readonly id: number は、
- プロパティ宣言
- コンストラクタ引数
this.id = idの代入- readonly 指定
を一気にやってくれます。
プロパティの型を「クラスの外」に出す設計
クラスの中に型を書きすぎると読みにくくなる
例えば、こういうクラスがあります。
class Order {
items: { id: number; name: string; price: number; quantity: number }[];
status: "pending" | "paid" | "shipped";
constructor(
items: { id: number; name: string; price: number; quantity: number }[],
status: "pending" | "paid" | "shipped"
) {
this.items = items;
this.status = status;
}
}
TypeScript動くし、型も正しいですが、
クラス宣言のところがかなりゴチャゴチャしています。
ここで効くのが「型を外に出して名前をつける」設計です。
型エイリアスやインターフェースを使って整理する
type OrderItem = {
id: number;
name: string;
price: number;
quantity: number;
};
type OrderStatus = "pending" | "paid" | "shipped";
class Order {
constructor(
public items: OrderItem[],
public status: OrderStatus
) {}
}
TypeScriptこうすると、
- クラスの宣言はスッキリする
OrderItemやOrderStatusを他の場所でも再利用できる
というメリットがあります。
「プロパティの型が長くなってきたな」と感じたら、
「この型に名前をつけて外に出せないか?」
と考えてみてください。
プロパティの型と「ドメインの意味」を結びつける
ただの string ではなく「意味のある型」にする
例えば、ユーザーのメールアドレスを string と書くのは簡単ですが、
もう一歩踏み込んで「意味のある型」にすることもできます。
type Email = string;
class User {
constructor(
public id: number,
public name: string,
public email: Email
) {}
}
TypeScript今は Email = string ですが、
将来「Email 型にだけ特別な処理をしたい」となったとき、
この型エイリアスが効いてきます。
同じように、
type UserId = number;
type Price = number;
type Quantity = number;
TypeScriptのように、「同じ number だけど意味が違うもの」に
名前をつけておくのもよくあるパターンです。
リテラル型・ユニオン型で「取りうる値」を絞る
さきほどの OrderStatus のように、
「取りうる値が限られている」プロパティは、
ユニオン型で絞ると安全になります。
type OrderStatus = "pending" | "paid" | "shipped";
class Order {
constructor(
public id: number,
public status: OrderStatus
) {}
}
const o = new Order(1, "pending");
// o.status = "unknown"; // エラー:OrderStatus にない値
TypeScript「このプロパティ、実際には3種類くらいしか値がないな」と思ったら、string ではなくリテラルユニオンにしてみると、
バグをかなり防げます。
配列・マップ・オブジェクト型のプロパティ設計
配列プロパティ:中身の型をしっかり決める
例えば、「ユーザーが持っているタグ」を表すとします。
class User {
constructor(
public id: number,
public name: string,
public tags: string[]
) {}
}
TypeScriptここで string[] としてもいいですが、
タグに意味を持たせたいなら型を分けてもいいです。
type Tag = string;
class User {
constructor(
public id: number,
public name: string,
public tags: Tag[]
) {}
}
TypeScriptさらに、「タグの種類が決まっている」なら、
リテラルユニオンにすることもできます。
type Tag = "admin" | "premium" | "beta";
class User {
constructor(
public id: number,
public name: string,
public tags: Tag[]
) {}
}
TypeScriptオブジェクトプロパティ:ネストし始めたら型を外に出す
例えば、住所を持つユーザー。
class User {
constructor(
public id: number,
public name: string,
public address: {
postalCode: string;
prefecture: string;
city: string;
line1: string;
line2?: string;
}
) {}
}
TypeScriptこれも、型を外に出すと読みやすくなります。
type Address = {
postalCode: string;
prefecture: string;
city: string;
line1: string;
line2?: string;
};
class User {
constructor(
public id: number,
public name: string,
public address: Address
) {}
}
TypeScript「プロパティの中にさらに構造がある」場合は、
その構造に名前をつけてあげると、
クラスの宣言がかなりスッキリします。
まとめ:プロパティの型設計を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
クラスのプロパティの型を設計するときは、
- まずは「素直な型」から始めていい
- 「あってもなくてもいい」か「必ずあるが空のこともあるか」で
?とnullを使い分ける - 変えちゃいけないものは
readonlyにする - 型が長くなってきたら、型エイリアスやインターフェースに外出しする
string/numberに意味を持たせたいときは、名前付きの型にする- 取りうる値が限られているなら、リテラルユニオンで絞る
今書いているクラスを1つ選んで、
「このプロパティ、本当はどういう意味の値なんだろう?」
「? / null / readonly をちゃんと選べているかな?」
と一度見直してみてください。
その小さな見直しが、
クラスを「ただ動くもの」から、
“意味がきちんと型に刻まれた設計図” に変えていきます。
