TypeScript | 関数・クラス・ジェネリクス:クラス設計 – プロパティの型設計

TypeScript TypeScript
スポンサーリンク

ゴール:「クラスのプロパティに“どんな型をつけるか”を意識して設計できるようになること」

クラスを書くとき、
「プロパティ名」だけでなく「プロパティの型」をどう設計するかで、
そのクラスの使いやすさ・安全さがかなり変わります。

ここでは、
「とりあえず 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", "こんにちは!");
TypeScript

bio?: 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 なので再代入できない
TypeScript

readonly を付けると、
「コンストラクタの中では代入できるけど、それ以外では変更できない」
というルールになります。

これは「ビジネスルールを型で表現する」強力な手段です。

  • ID
  • 作成日時
  • 生成時に決まって以降変わらないフラグ

などは、積極的に readonly を検討してみてください。

コンストラクタ省略記法+readonly

省略記法と組み合わせると、こう書けます。

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

public 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

こうすると、

  • クラスの宣言はスッキリする
  • OrderItemOrderStatus を他の場所でも再利用できる

というメリットがあります。

「プロパティの型が長くなってきたな」と感じたら、
「この型に名前をつけて外に出せないか?」
と考えてみてください。


プロパティの型と「ドメインの意味」を結びつける

ただの 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 をちゃんと選べているかな?」
と一度見直してみてください。

その小さな見直しが、
クラスを「ただ動くもの」から、
“意味がきちんと型に刻まれた設計図” に変えていきます。

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