TypeScriptで型を自作!ユーティリティ型とMapped Typesの使い方パターン集
生徒
「TypeScriptを使っていると、すでにある型を少しだけ作り替えたいことがよくあります。例えば、全部の項目を入力必須から任意に変えたいときとか。一つずつ書き直すしかないんでしょうか?」
先生
「そんなときこそ、ユーティリティ型の出番です。TypeScriptには便利な道具があらかじめ用意されていますが、実はそれを自分で作ることもできるんですよ。」
生徒
「型を自分で作る、ですか?難しそうですが、どうすればいいんですか?」
先生
「マッピングという考え方を使うと、驚くほど簡単に型を変換できます。まずは基本から一緒に学んでいきましょう!」
1. 型を再利用する重要性とは?
プログラミングにおいて、同じことを何度も書かないというのは非常に大切なルールです。これを「DRY原則」と呼びます。TypeScriptの世界でも、一度作った「型」を色々な場所で使い回すことが推奨されています。
例えば、ユーザー情報を扱う型を作ったとします。会員登録のときは全ての情報が必要ですが、プロフィール更新のときは名前だけ変更したい、という場合があります。このとき、登録用の型と更新用の型を別々に作ってしまうと、後で項目が増えたときに両方の修正が必要になり、ミスの原因になります。そこで、既存の型を変換して新しい型を自動的に作る仕組みが必要になるのです。
2. ユーティリティ型を自作するための基本知識
TypeScriptには、あらかじめ用意された便利な「ユーティリティ型」がありますが、これらを理解するためには「ジェネリクス」という概念を知っておく必要があります。ジェネリクスとは、型を「変数」のように扱う仕組みのことです。アルファベットのTなどを使って表現されることが多いです。
次に大切なのが「Mapped Types(マップドタイプス)」です。これは、オブジェクトのプロパティ(項目名)を一つずつ取り出して、新しいルールを適用しながら別の型を作る方法です。初心者の方には「型の繰り返し処理」と考えると分かりやすいでしょう。これらを組み合わせることで、自分だけの便利な型製造機を作ることができます。
3. 全ての項目を読み取り専用にする型を作る
まずは、オブジェクトの全ての項目を「読み取り専用」にする自作ユーティリティ型を考えてみましょう。読み取り専用にすると、後からその値を書き換えることができなくなります。これは、大事な設定データなどが誤って上書きされるのを防ぐために役立ちます。
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
type User = {
name: string;
age: number;
};
const myUser: MyReadonly<User> = {
name: "たろう",
age: 25
};
// myUser.name = "じろう"; // ここでエラーになります
このコードでは、「keyof T」を使ってUser型にある「name」と「age」という名前を取り出し、それを「P」という変数に入れています。そして、その前に「readonly」をつけることで、全ての項目に鍵をかけているイメージです。
4. 全ての項目を省略可能にする型を作る
次に、全ての項目を「あってもなくても良い(任意)」という状態にする型を作ってみましょう。これを「Partial(パーシャル)」と呼びます。更新画面などで、一部の項目だけ入力すれば良いという状況で非常に便利です。型定義の項目名の後ろに「ハテナマーク」をつけるのがポイントです。
type MyOptional<T> = {
[P in keyof T]?: T[P];
};
type Book = {
title: string;
price: number;
};
const partialBook: MyOptional<Book> = {
title: "TypeScript入門"
// priceがなくてもエラーになりません
};
ここでは「?」記号を使うことで、TypeScriptに対して「この項目は入力しなくても大丈夫だよ」と伝えています。自作することで、標準のPartial型がどのような仕組みで動いているのかがよく分かりますね。
5. 特定の型のみを抽出するフィルター型
さらに応用として、特定の条件に合うものだけを抜き出す型を作ってみましょう。例えば、たくさんの項目の中から「文字列(string)」だけを抜き出したい場合があります。これには「Conditional Types(条件付き型)」という仕組みを使います。プログラミングのif文のように、「もしこの型ならこれ、そうでなければこれ」という指定ができます。
type OnlyString<T> = T extends string ? T : never;
type MyResult = OnlyString<string | number | boolean>;
// MyResultは string型 になります
「never」というのは、TypeScriptにおいて「何も存在しない」ことを表す特別な型です。条件に合わないものをneverにすることで、実質的にその型をリストから消し去ることができるのです。これを使うと、複雑なデータの絞り込みが可能になります。
6. Mapped Typesでプロパティ名を一括変換する
Mapped Typesの面白いところは、項目の名前そのものを書き換えることができる点です。例えば、元の型にある項目名の前に「get_」という文字を付け加えた新しい型を作ることができます。これは、データの取得用関数を自動生成する際などに役立ちます。テンプレートリテラル型という機能を使います。
type Getter<T> = {
[P in keyof T as `get_${string & P}`]: () => T[P];
};
type Person = {
name: string;
age: number;
};
type PersonGetters = Getter<Person>;
/*
PersonGettersの中身はこうなります:
{
get_name: () => string;
get_age: () => number;
}
*/
「as」というキーワードを使うことで、元の名前「P」を新しい名前にリネームしています。これにより、手作業で一つずつメソッド名を定義する手間が省け、名前の付け間違いというケアレスミスも防ぐことができます。
7. 自作型を組み合わせて高度な型を作る
ここまで紹介したテクニックは、複数を組み合わせることでさらに真価を発揮します。例えば、「読み取り専用にしつつ、全ての項目を必須にする」といった型も作れます。必須にするには、マイナス記号「-?」を使います。これは「ハテナ(任意)の状態を取り除く」という意味になります。
パソコンの操作に例えると、フォルダの属性を一括で変更するような感覚です。ファイル一つ一つを右クリックして変更するのは大変ですが、コマンド一つでフォルダ内の全ファイルを書き換え禁止にするような、強力なパワーがTypeScriptの自作型には備わっています。最初は難しく感じるかもしれませんが、パズルのように型を組み立てる感覚に慣れてくると、プログラミングがどんどん楽しくなっていくはずです。
8. エラーを防ぐための型安全性の向上
なぜここまでして型を自作するのでしょうか。それは、開発中の「うっかりミス」をコンピュータに指摘してもらうためです。TypeScriptは、私たちが書いたコードが正しいルールに従っているかを常に監視してくれます。型を細かく定義すればするほど、プログラムを実行する前に間違いを見つけることができます。
特に大規模な開発では、数百、数千のファイルが存在します。誰かが定義した型を別の誰かが使うとき、自作ユーティリティ型によって適切に制限がかけられていれば、使い方の間違いに即座に気づけます。初心者のうちから「どうすればもっと安全な型を作れるか」を意識することは、プロフェッショナルなエンジニアへの第一歩となるでしょう。まずは簡単な「読み取り専用」や「省略可能」の自作から始めて、徐々に複雑な変換に挑戦してみてください。