ゴール:「コンストラクタの引数に“どんな型をつけるか”を意識して設計できるようになること
クラスを書くとき、
コンストラクタは「インスタンスをどう初期化するか」を決める、とても重要な場所です。
ここでの型設計が雑だと、
- 作るときに毎回つらい
- 間違った値でも通ってしまう
- 後から変更しづらい
みたいな地味なストレスが積み上がります。
逆に、コンストラクタ引数とその型をちゃんと設計できると、
「このクラスはこういう前提で使うんだな」が自然と伝わるようになります。
順番に、具体例でかみ砕いていきます。
基本形:コンストラクタ引数とプロパティの関係
一番素直なパターン:「引数をそのままプロパティに入れる」
まずは、いちばん基本の形から。
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
const u = new User(1, "Taro");
TypeScriptここでやっていることはシンプルです。
- コンストラクタ引数
id: number→ プロパティthis.id - コンストラクタ引数
name: string→ プロパティthis.name
「このクラスのインスタンスを作るには、
id(数値)と name(文字列)が必須です」という前提を、
コンストラクタの型がそのまま表しています。
ここでのポイントは、
「コンストラクタ引数の型 = インスタンスの“最低限の初期状態”」
という意識を持つことです。
TypeScript の省略記法:引数にアクセス修飾子をつける
TypeScript では、よくあるこのパターンを短く書けます。
class User {
constructor(
public id: number,
public name: string
) {}
}
const u = new User(1, "Taro");
TypeScriptpublic id: number と書くと、
- プロパティ宣言(
id: number;) - コンストラクタ引数
this.id = idの代入
をまとめてやってくれます。
private や readonly も同じように使えます。
class User {
constructor(
public readonly id: number,
private name: string
) {}
}
TypeScriptこの省略記法は便利ですが、
「何が起きているか」を理解したうえで使うと、
クラスの構造が頭に入りやすくなります。
必須・任意・デフォルト値:引数の型で“前提”を表現する
必須引数:このクラスを作るのに絶対必要な情報
たとえば、ユーザーの id と name は必須だとします。
class User {
constructor(
public id: number,
public name: string
) {}
}
new User(1, "Taro"); // OK
// new User(1); // エラー:name がない
TypeScript「このクラスは、id と name がないと成立しない」
という前提を、コンストラクタの型が保証してくれます。
“このクラスのインスタンスが存在するなら、必ず持っていてほしい情報”
は、コンストラクタの必須引数にします。
任意引数:あってもなくてもいい情報(?)
たとえば、自己紹介文 bio は任意だとします。
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 は「渡されないこともある」ことを意味します。
ここで大事なのは、
「本当に“作るときに任意”にしたいのか?」
を一度考えることです。
- 「あとから必ず設定する前提」なら、コンストラクタで必須にしてもいい
- 「本当にあってもなくてもいい」なら、
?で任意にする
コンストラクタの引数は、
「このクラスを作るときに、何を“約束”させたいか」を表現する場所です。
デフォルト値付き引数:よくあるパターンを型で楽にする
たとえば、ユーザーのステータスを active / inactive で持つとして、
デフォルトは active にしたいとします。
type UserStatus = "active" | "inactive";
class User {
constructor(
public id: number,
public name: string,
public status: UserStatus = "active"
) {}
}
const u1 = new User(1, "Taro"); // status は "active"
const u2 = new User(2, "Hanako", "inactive"); // 明示的に指定
TypeScriptデフォルト値を付けると、
- 呼び出し側は「よくあるケース」では引数を省略できる
- 型としては
status: UserStatusのまま(undefinedにはならない)
というメリットがあります。
「ほとんどのケースで同じ値を渡すな…」と感じたら、
デフォルト値を検討してみてください。
「オブジェクト1個で受ける」か「引数をバラで受ける」か
バラで受けるパターン:シンプルだが増えるとつらい
ここまでの例は、すべて「引数をバラで受ける」形でした。
class User {
constructor(
public id: number,
public name: string,
public email: string,
public status: UserStatus = "active"
) {}
}
TypeScript引数が3〜4個くらいまではこれで十分ですが、
5個、6個と増えてくると、
- 呼び出し側で順番を間違えやすい
- 何が何の値か一目で分かりづらい
という問題が出てきます。
オブジェクト1個で受けるパターン:意味が伝わりやすい
そこでよく使うのが、「オブジェクト1個で受ける」パターンです。
type UserParams = {
id: number;
name: string;
email: string;
status?: UserStatus;
};
class User {
id: number;
name: string;
email: string;
status: UserStatus;
constructor(params: UserParams) {
this.id = params.id;
this.name = params.name;
this.email = params.email;
this.status = params.status ?? "active";
}
}
const u = new User({
id: 1,
name: "Taro",
email: "taro@example.com",
status: "inactive",
});
TypeScriptこの形のメリットは、
- 呼び出し側で「プロパティ名付き」で渡せるので、意味が分かりやすい
- 引数が増えても「順番」を気にしなくていい
UserParams型を他の場所でも再利用できる
というところです。
「引数が4つを超え始めたら、オブジェクト1個で受ける形を検討する」
くらいの感覚を持っておくと、だいぶ楽になります。
コンストラクタ引数と「不変なもの/変わるもの」
「作ったあと変えないもの」はコンストラクタで必須+readonly
たとえば、ユーザーの id は一度決まったら変えないとします。
class User {
constructor(
public readonly id: number,
public name: string
) {}
}
const u = new User(1, "Taro");
// u.id = 2; // エラー:readonly
u.name = "Jiro"; // OK
TypeScriptここでの設計はこうです。
idは「生成時に必須」「その後は不変」nameは「生成時に必須」「その後も変更可能」
「この値は、いつ決まって、いつまで変わりうるのか?」
を考えて、コンストラクタ引数と readonly を組み合わせると、
クラスのルールが型に刻まれます。
「あとから設定される前提のもの」はコンストラクタに含めない選択肢もある
たとえば、「ユーザーの最終ログイン日時」は、
作成時にはまだ決まっていないかもしれません。
class User {
lastLoginAt?: Date;
constructor(
public readonly id: number,
public name: string
) {}
updateLastLogin(date: Date) {
this.lastLoginAt = date;
}
}
TypeScriptこういう「あとから決まるもの」は、
無理にコンストラクタ引数に入れず、
メソッドで設定する設計もアリです。
コンストラクタに入れるのは、
「このインスタンスが“存在する”なら、最初から持っていてほしい情報」
に絞ると、設計がスッキリします。
ジェネリクスとコンストラクタ引数
型パラメータをコンストラクタ引数に反映する
少しだけ進んだ話も触れておきます。
たとえば、「値を1つ持つ Box クラス」をジェネリクスで書くとします。
class Box<T> {
constructor(
public value: T
) {}
}
const n = new Box<number>(123);
const s = new Box("hello"); // 型推論で Box<string>
TypeScriptここでのポイントは、
- クラスの型パラメータ
T - コンストラクタ引数
value: T - プロパティ
value: T
がすべて連動していることです。
コンストラクタ引数の型にジェネリクスを使うことで、
「このクラスは“何を包む Box なのか”」を柔軟に変えられます。
コンストラクタ引数から型パラメータを推論させる
さっきの Box は、型引数を省略しても動きます。
const n = new Box(123); // Box<number>
const s = new Box("hello"); // Box<string>
TypeScriptコンストラクタ引数の型から、
TypeScript が T を推論してくれるからです。
「コンストラクタ引数の型」と「クラスのジェネリクス」は、
セットで設計するととても強力になります。
まとめ:コンストラクタ引数と型を自分の言葉で整理すると
最後に、あなた自身の言葉でこう整理してみてください。
コンストラクタ引数の型は、
- 「このクラスのインスタンスを作るときに、何を必ず渡させたいか」
- 「どこまでを“初期状態の約束”にするか」
を表現する場所。
設計するときは、
- 必須/任意/デフォルト値を意識して決める
- 引数が増えてきたらオブジェクト1個で受ける形も検討する
- 不変なものは
readonly+コンストラクタ必須にする - 「あとから決まるもの」は無理にコンストラクタに入れない
- 型エイリアス(
UserParamsなど)を使って意味を名前で表す
あたりを意識してみてください。
今書いているクラスのコンストラクタを1つ選んで、
「本当にこの引数は全部必要?」
「これは任意にしてもいい?」
「これは readonly にすべきじゃない?」
と問い直してみると、
クラスの“前提条件”が一段クリアに見えてきます。
