📄

【TypeScript】ユーティリティ型を自作して、型の理解を深める

2022/05/21に公開

どうもフロントエンドエンジニアのoreoです。

みなさん、type-challengesをご存じでしょうか?type-challengesとは、TypeScriptの型問題が記載されているリポジトリです。easy、medium、hard、extremeの4つのレベルに別れており、全部で140問ほど、出題されています。

https://github.com/type-challenges/type-challenges

今回は、type-challengesを参考にしながら、TypeScriptの代表的なユーティリティ型を自分で定義し、型の理解を深めたいと思います。

この記事では、ユーティリティ型のPartialRequiredReadonlyPickOmitExcludeを扱います!

1 Partial<T>

1-1 書き換え

Partial<T>は、Tの全てのプロパティをオプショナルにするユーティリティ型です。

type CustomerData = {
  name: string;
  age: number;
  gender?: string;
};

type PartialCustomerData = Partial<CustomerData>;

PartialCustomerDataは👇のようになります。

type PartialCustomerData = {
  name?: string;
  age?: number;
  gender?: string;
};

Partial<T>は、👇のMyPartial<T>のように書き換えることができます。

type MyPartial<T> = { 
  [P in keyof T]?: T[P]; 
};

type PartialCustomerData = MyPartial<CustomerData>;

1-2 ポイント

ここでのポイントはHomomorphic mapped typeです。これは、{ [P in keyof T] : U }のような形をとりオブジェクト型TのプロパティPに対して、型Uを持つようなオブジェクト型を生成します。

Homomorphic mapped typeに関しては、👇の記事をご覧ください。

型パズルで大活躍!! Mapped typeについて

2 Required<T>

2-1 書き換え

Required<T>は、Tの全てのプロパティを必須にするユーティリティ型です。

type CustomerData = {
  name: string;
  age: number;
  gender?: string;
};

type RequiredCustomerData = Required<CustomerData>;

RequiredCustomerDataは、👇のようになります。

type RequiredCustomerData = {
  name: string;
  age: number;
  gender: string;
};

Required<T>は、👇のMyRequired<T>のように書き換えることができます。

type MyRequired<T> = { 
  [P in keyof T]-?: T[P]; 
};
type RequiredCustomerData = MyRequired<CustomerData>;

2-2 ポイント

ポイントは、1-2のHomomorphic mapped typeに加えて、-?です。プロパティに-?をつけると、プロパティが必須になります。

3 Readonly<T>

3-1 書き換え

Readonly<T>は、Tの全てのプロパティを読み取り専用にするユーティリティ型です。

type CustomerData = {
  name: string;
  age: number;
  gender?: string;
};

type ReadonlyCustomerData = Readonly<CustomerData>;

ReadonlyCustomerDataは、👇のようになります。

type ReadonlyCustomerData = {
   readonly name: string;
   readonly age: number;
   readonly gender?: string;
}

Readonly<T>は、👇のMyReadonly<T>のように書き換えることができます。

type MyReadonly<T> = {
  readonly [P in keyof T] :T[P];
}
type ReadonlyCustomerData = MyReadonly<CustomerData>;

3-2 ポイント

ポイントは、1-2のHomomorphic mapped typeに加えて、readonlyを付けてあげると、それぞれのプロパティを読み取り専用にしてくれます。

ちなみに、readonlyを削除したい場合は、プロパティの前に-readonlyを付ければ、削除することが可能です。

4 Pick<T,Keys>

4-1 書き換え

Pick<T,Keys>は、型TからKeysで指定したプロパティを含むオブジェクト型を返すユーティリティ型です。

type CustomerData = {
  name: string;
  age: number;
  gender?: string;
};

type CustomerNameLists = Pick<CustomerData,"name" | "age">

CustomerNameListsは、👇のようになります。

type CustomerNameLists = {
    name: string;
    age: number;
}

Pick<T,Keys>は、👇のMyPick<T, Keys extends keyof T>のように書き換えることができます。

type MyPick<T, Keys extends keyof T> = {
  [key in Keys] : T[key]
}
type CustomerNameLists = MyPick<CustomerData,"name" | "age">

4-2 ポイント

ポイントは、Keys extends keyof T とMapped typeです。extends keyofで、KeysTのプロパティのユニオン型として制限をかけています。

また、Mapped typeで、ユニオン型Keysの構成要素keyをプロパティとし、それに対して、オブジェクト型Tのプロパティkeyが持つ型を設定しています。

Mapped typeに関しては、👇の記事をご覧ください。

型パズルで大活躍!! Mapped typeについて

5 Omit<T,keys>

5-1 書き換え

Omit<T,Keys>は、Tからユニオン型Keysで指定したプロパティを除くオブジェクト型を返すユーティリティ型です。

type CustomerData = {
  name: string;
  age: number;
  gender?: string;
};

type CustomerNameLists = Omit<CustomerData,"age" | "gender">

CustomerNameListsは、👇のようになります。

type CustomerNameLists = {
    name: string;
}

Omit<T,Keys>は、👇のMyOmit<T, Keys extends keyof T>のように書き換えることができます。

type MyOmit<T, Keys extends keyof T> = {
  [Key in keyof T as Key extends Keys ? never : Key]: T[Key]
}

type CustomerNameLists = MyOmit<CustomerData,"age" | "gender">

5-2 ポイント

ポイントは、as Key extends Keys ? never : Keyの部分です。

ここでは、Conditional Typeを使って、型の条件分岐を行っており、Tのプロパティであるkeyが、Keysのサブタイプであればnever型を返し、サブタイプでなければkeyを返します。プロパティに対して、asneverをアサーションすると、プロパティを削除することができます。

Conditional Typeについては、👇の記事をご覧ください。

Conditional Typeで型の条件分岐を行う

6 Exclude<T,U>

6-1 書き換え

Exclude<T,U>は、ユニオン型Tから、Uで指定した型を除いたユニオン型を返すユーティリティ型です。

type JobGrade = "S" | "A" | "B" | "C" | "D";
type SuperJobGrade = Exclude<JobGrade, "B" | "C" | "D">;

SuperJobGradeは、👇のようになります。

type SuperJobGrade = "S" | "A"

Exclude<T,U>は、👇のMyExclude<T, U>のように書き換えることができます。

type MyExclude<T, U > = T extends U ? never : T

type SuperJobGrade = MyExclude<JobGrade, "B" | "C" | "D">;

6-2 ポイント

ポイントは、Conditional TypeであるT extends U ? never : TでのUnion distribution(ユニオンの分配)です。Conditional Typeでは、Tがユニオン型の場合、Tのそれぞれの要素に対して、Conditional Typeが適用されます。そのためUのサブタイプである、Tの要素には、neverが返され、型の削除が可能になります。

Conditional Typeの分配法則については、👇の記事をご覧ください。

Conditional Typeで型の条件分岐を行う

7 最後に

実際自分で手を動かすと理解が格段に深まりますね。type-challengesに、たくさん問題があるので興味のある方は是非トライしてみてください!私はまたmediumの途中ですが、隙間時間に進めたいです!

8 参考

https://github.com/type-challenges/type-challenges

Discussion