0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】typeのプロパティの型を他プロパティの値で条件分岐させる

Posted at

はじめに

例えば、Userという型に、「管理者」と「スタッフ」の2つのロールがあるとします。

type
type UserType = {
    role: "admin" | "staff";
    admin_num?: num; // 管理者なら必須
    // 以下共通
    id: number;
    name: string;
    email: string;
}

管理者なら(roleプロパティが"admin"なら)、管理者番号(admin_numプロパティ)を必須にしたいですが、このtypeだと制約できません。

// admin_numがなくてもコンパイルが通る
const admin: UserType = {
    role: "admin",
    id: 1,
    name: "Admin Example",
    email: "email"
}

というように、「あるプロパティの型を、同じtype内にある他プロパティの値ごとに変える」型宣言をしたいケースがあると思います。

本記事では、TypeScriptでそれを実装する方法についてまとめます。

条件になるプロパティが、リテラル型の場合に限ります

環境

  • typescript: v5.8.3

型の実装方法

1. Union型

実装方法の一つは、Union型を用いることです。TypeScriptは条件となるプロパティによって、Union型のメンバーを判別し特定できます。

やりたいことは↑の記事で書かれている通りですが、冒頭の例を書き直すとこうなります。

type
// 共通部
type UserInfoType = {
    id: number;
    name: string;
    email: string;
}

type AdminType = UserInfoType & {
    role: "admin";
    admin_num: number;
}

type StaffType = UserInfoType & {
    role: "staff";
}

type UserUnionType = AdminType | StaffType // ← Userの型

この型を用いれば、型制約を満たしていないとコンパイルエラーになります。

const admin: UserUnionType = {
      ~~~~~
    role: "admin",
    id: 1,
    name: "Admin One",
    email: "email"
}
コンパイルエラー
error TS2322: Type '{ role: "admin"; id: number; name: string; email: string; }' is not assignable to type 'UserUnionType'.
  Type '{ role: "admin"; id: number; name: string; email: string; }' is not assignable to type 'AdminType'.
    Property 'admin_num' is missing in type '{ role: "admin"; id: number; name: string; email: string; }' but required in type '{ role: "admin"; admin_num: number; }'.

これで意図した型制約を実装することができました。
宣言時の制約以外にも、if文やswitch文によるroleの分岐で、型の絞り込みもできます。

const user: UserUnionType = { ... }
if (user.role === "staff") return

// user is AdminType
user.admin_num // number, not undefined

2. Genericsを使う

先程と同じ型を、GenericsとConditional Typesを用いて記述することもできます。

type
type UserRoleType = "admin" | "staff";

type UserRoleGenericsType<T extends UserRoleType> = T extends "admin" ?
{
    role: T;
    admin_num: number;
} :
{
    role: T;
}

type UserGenericsType = {
    id: number;
    name: string;
    email: string;
} & UserRoleGenericsType<UserRoleType>
// 以下はコンパイルエラー
const staff: UserGenericsType = {
    role: "staff",
    admin_num: 2,
    ~~~~~~~~~
    id: 2,
    name: "Staff Two",
    email: "email"
}

Union型の実装と同様に型の絞り込みもできます。

ただ、こちらの場合ロールの種類が増えると、Conditional Typesでextends地獄が始まります。また、各ロールごとのtypeも必要になったら作る必要があります。

type
type ExtractAdminType = Extract<UserGenericsType, { role: "admin" }>
type ExtractStaffType = Extract<UserGenericsType, { role: "staff" }>

注意

UserRoleGenericsTypeにあるrole: Tは、一見同じプロパティとして共通化できそう(以下コード参照)ですが、この実装ではうまく判定されません。

型制約できない例
type UserRoleGenericsType<T extends UserRoleType> = T extends "admin" ?
{ admin_num: number; }
: {}

type UserGenericsType = {
    id: number;
    name: string;
    email: string;
    role: UserRoleType; // 共通なので移動 → 型制約できなくなる
} & UserRoleGenericsType<UserRoleType>

つまり、外部のtypeのプロパティを条件にしたいときは、分岐先のtypeの方にもそのプロパティの宣言が必要です。

条件になるプロパティが外部のtypeにある
// 外部からimport
type ImportedUserInfoType = {
    id: number;
    name: string;
    email: string;
    role: UserRoleType;
}

---

type UserRoleGenericsType<T extends UserRoleType> = T extends "admin" ?
{
    role: T; // 分岐先でも宣言
    admin_num: number;
} :
{
    role: T;
}

type UserType = ImportedUserInfoType & UserRoleGenericsType<UserRoleType>

おわりに

以上が「あるプロパティの型を、他プロパティの値ごとに変える」型宣言をする方法でした。

最初自分は、2.のGenericsを使う方で実装してましたが、こう見るとUnion型の方がシンプルで良さそうだと感じました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?