TypeScript | 関数・クラス・ジェネリクス:クラス設計 – コンストラクタ引数と型

TypeScript TypeScript
スポンサーリンク

ゴール:「コンストラクタの引数に“どんな型をつけるか”を意識して設計できるようになること

クラスを書くとき、
コンストラクタは「インスタンスをどう初期化するか」を決める、とても重要な場所です。

ここでの型設計が雑だと、

  • 作るときに毎回つらい
  • 間違った値でも通ってしまう
  • 後から変更しづらい

みたいな地味なストレスが積み上がります。

逆に、コンストラクタ引数とその型をちゃんと設計できると、
「このクラスはこういう前提で使うんだな」が自然と伝わるようになります。

順番に、具体例でかみ砕いていきます。


基本形:コンストラクタ引数とプロパティの関係

一番素直なパターン:「引数をそのままプロパティに入れる」

まずは、いちばん基本の形から。

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");
TypeScript

public id: number と書くと、

  • プロパティ宣言(id: number;
  • コンストラクタ引数
  • this.id = id の代入

をまとめてやってくれます。

privatereadonly も同じように使えます。

class User {
  constructor(
    public readonly id: number,
    private name: string
  ) {}
}
TypeScript

この省略記法は便利ですが、
「何が起きているか」を理解したうえで使うと、
クラスの構造が頭に入りやすくなります。


必須・任意・デフォルト値:引数の型で“前提”を表現する

必須引数:このクラスを作るのに絶対必要な情報

たとえば、ユーザーの idname は必須だとします。

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", "こんにちは");
TypeScript

bio?: 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 にすべきじゃない?」

と問い直してみると、
クラスの“前提条件”が一段クリアに見えてきます。

タイトルとURLをコピーしました