【入門編】 Mapped Typesにおける?修飾子の付与と削除 – TypeScript実践ガイド

やあ、皆さん!TypeScriptの世界へようこそ。伝説のチーフアーキテクト、私がお話ししましょう。

今日はね、TypeScriptの型定義の中でも、特に「Mapped Types」という、ちょっと魔法みたいな機能の中から、プロパティを「あってもなくてもOK」にする`?`(オプショナル)と、「やっぱり必須!」に戻す`-?`(必須化)のテクニックについて、深掘りしていこうと思います。

「なんか難しそう…」って思いました? 大丈夫、大丈夫。私と一緒に、一つずつ丁寧に紐解いていけば、きっと「なるほど!」って膝を打つ瞬間が訪れますから。現場のリアルな知見も交えながら、優しくお話ししていきますね。

Mapped Typesって、そもそも何者?

本題に入る前に、まずは「Mapped Types」って何?ってところから、フワッと理解しておきましょう。

TypeScriptの世界には、オブジェクトの「形」を定義する「型」がありますよね。例えば、ユーザー情報だったり、商品のデータだったり。

// ユーザー情報を表現する型
type User = {
id: number; // ユーザーIDは数値
name: string; // 名前は文字列
email: string; // メールアドレスも文字列
isActive: boolean; // アクティブ状態は真偽値
};

この`User`型、とってもシンプルで分かりやすいですよね。
Mapped Typesというのは、既存の型(この場合は`User`)を「設計図」にして、新しい型の「設計図」を自動的に作り出す、賢い「型変換マシン」だとイメージしてください。

例えば、「`User`型を元に、すべてのプロパティが読み取り専用になった新しい型を作りたい!」とか、「すべてのプロパティが、あってもなくてもいい(オプショナル)型を作りたい!」なんていう時に、このマシンが大活躍するんです。一つ一つ手作業で新しい型を定義し直すなんて、泥臭い作業は、このTypeScriptの現場ではご法度ですよ!

プロパティを「あってもなくてもOK」にする `?` の魔法

さて、本題に入りましょう。まずは「`?`」の魔法です。これは皆さん、普段の型定義でもお馴染みかもしれませんね。

type Product = {
id: string;
name: string;
price: number;
description?: string; // ここに注目!`?`が付いているので、descriptionはあってもなくてもOK
};

// descriptionプロパティがなくてもエラーにならない
const myProduct: Product = {
id: “P001”,
name: “スペシャルコーヒー豆”,
price: 1200,
// descriptionがなくてもOK!
};

// descriptionプロパティがあってもOK
const anotherProduct: Product = {
id: “P002”,
name: “オリジナルマグカップ”,
price: 800,
description: “手に馴染む、こだわりの一品です。”, // あってもOK!
};

この`?`は、プロパティが「オプショナル(Optional)」、つまり「任意」であることを示します。例えるなら、お店で商品を買うときの「レシートの有無」みたいなものです。欲しい人はもらうし、いらない人はもらわない。どちらでも良い、ってことですね。

Mapped Typesで「全部オプショナル」にする!

この「`?`」の魔法を、Mapped Typesを使って一気に、しかも自動的に付与することができるんです。その代表格が、TypeScriptに最初から用意されている`Partial`というユーティリティ型です。

`Partial`は、「`T`型のすべてのプロパティをオプショナルにする」という役割を持っています。

例えるなら、お店で「商品の注文書」を書くとき、通常は全部の項目を埋めないといけないですよね? でも、`Partial`を使うと、その注文書をコピーして、「どの項目も仮でOK!記入しなくてもOKな『仮注文書』」を自動で作ってくれる、そんなイメージです。

実際のコードを見てみましょう。

// ユーザー情報を表現する型 (先ほどのUser型を再利用)
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};

// Partialを使って、すべてのプロパティをオプショナルにする
type PartialUser = Partial;

/
PartialUser型は、実質的に以下と同じ意味になります
type PartialUser = {
id?: number;
name?: string;
email?: string;
isActive?: boolean;
};
/

// PartialUser型は、どのプロパティも定義しなくてもエラーにならない
const partialUserData: PartialUser = {
// nameプロパティだけ定義してもOK!
name: “山田 太郎”,
};

// もちろん、全てのプロパティを定義してもOK!
const fullPartialUserData: PartialUser = {
id: 1,
name: “佐藤 花子”,
email: “hana@example.com”,
isActive: true,
};

// idプロパティだけ定義してもOK!
const idOnlyData: PartialUser = {
id: 99,
};

console.log(partialUserData); // { name: ‘山田 太郎’ }
console.log(fullPartialUserData); // { id: 1, name: ‘佐藤 花子’, email: ‘hana@example.com’, isActive: true }
console.log(idOnlyData); // { id: 99 }

どうですか? `Partial`と書くだけで、`User`型が持っていた`id`, `name`, `email`, `isActive`の全てのプロパティに`?`が付いて、オプショナルな型に変換されました。

この`Partial`の内部構造は、実はこんな風になっています。

type Partial = {
[P in keyof T]?: T[P]; // ここがポイント!
};

ちょっと呪文みたいに見えますよね? 大丈夫、分解して解説します。

  • `[P in keyof T]`: これは「`T`型が持っているプロパティの名前を一つずつ取り出して、それらを`P`という変数に入れる」という意味です。`keyof T`は`T`型が持つ全てのプロパティ名(キー)の集合を表します。
  • `?:`: そして、取り出したそれぞれのプロパティ`P`に、この`?`(オプショナル修飾子)を付けます。
  • `T[P]`: そのプロパティ`P`の元の型(`T`型における`P`の型)をそのまま使います。

つまり、「`T`型から全てのプロパティ名を引っ張り出してきて、その一つ一つに`?`をつけて、元の型をそのまま適用した新しい型を作る」というわけです。なるほど、賢い「型変換マシン」ですよね!

プロパティを「やっぱり必須!」に戻す `-?` のチカラ

さて、ここからが今日のメインテーマの一つ、`-?`修飾子のお話です。
一度オプショナルになったプロパティを、「やっぱりこれ、必須にしたい!」と元に戻す、あるいは、もともとオプショナルだったものを強制的に必須にする、そんな時にこの`-?`が活躍します。

例えるなら、「仮注文書」では記入が任意だった項目を、「本注文書にするから、ここだけは絶対書いてね!」と、特定の部分を強制的に必須に戻すようなイメージです。

TypeScriptには、すべてのプロパティを必須に戻す`Required`というユーティリティ型が用意されています。

// 商品情報を表現する型
type Item = {
id: string;
name: string;
price?: number; // priceはオプショナル
category: string;
};

// Item型をPartialで一度、全てのプロパティをオプショナルにする
type AllOptionalItem = Partial;

/
AllOptionalItem型は以下と同じ
type AllOptionalItem = {
id?: string;
name?: string;
price?: number;
category?: string;
};
/

// ここからが本番!AllOptionalItem型をRequiredで、全てのプロパティを必須に戻す
type AllRequiredItem = Required;

/
AllRequiredItem型は以下と同じ
type AllRequiredItem = {
id: string;
name: string;
price: number; // priceも必須に戻った!
category: string;
};
/

// AllRequiredItem型は、全てのプロパティが必須なので、一つでも欠けるとエラーになる
const completeItem: AllRequiredItem = {
id: “I001”,
name: “高級紅茶”,
price: 3000, // priceプロパティも必須!
category: “飲料”,
};

// これはエラーになります! priceとcategoryが足りません。
/
const incompleteItem: AllRequiredItem = {
id: “I002”,
name: “ティーカップ”,
};
/

console.log(completeItem); // { id: ‘I001’, name: ‘高級紅茶’, price: 3000, category: ‘飲料’ }

`Required`とすることで、元々はオプショナルだった`price`プロパティも含め、全てのプロパティが必須の型に変換されました。

この`Required`の内部構造は、こうなっています。

type Required = {
[P in keyof T]-?: T[P]; // ここがポイント!`-?`
};

これも先ほどの`Partial`と同じように分解してみましょう。

  • `[P in keyof T]`: これは`Partial`と同じで、「`T`型が持っているプロパティの名前を一つずつ取り出して、それらを`P`という変数に入れる」ですね。
  • `-?:`: ここがミソです! この`-`(マイナス記号)は、「オプショナル修飾子`?`を取り除く」という意味なんです。つまり、もしプロパティに`?`が付いていたら、それを削除して必須に戻す、という指示になります。
  • `T[P]`: プロパティ`P`の元の型をそのまま使います。

つまり、「`T`型から全てのプロパティ名を引っ張り出してきて、もし`?`が付いていたらそれを取り除き、元の型をそのまま適用した新しい型を作る」というわけです。

この`-?`は、TypeScript 2.8から導入された「修飾子の削除(Removal of Modifiers)」という強力な機能の一部なんです。`readonly`修飾子を外す`-readonly`もありますよ。現場では、一時的に読み取り専用にしたいけど、特定の処理では書き込みを許可したい、なんて時に重宝します。

現場でどう使う? 具体的なシナリオ

さて、こんな魔法みたいな機能、実際の開発現場でどう役立つのか、気になりますよね?

例えば、Webアプリケーションでユーザー情報を更新するAPIを考えてみましょう。

1. ユーザー登録時: 全ての情報(名前、メールアドレスなど)が必須です。
2. ユーザー情報更新時: ユーザーが変更したい項目だけを送ればよく、全ての項目を送る必要はありません。

この「更新時」のデータ型を定義する際に、`Partial`が非常に役立ちます。

type UserProfile = {
userId: string;
userName: string;
email: string;
birthDate: string; // 例えば ‘YYYY-MM-DD’ 形式
profileImage?: string; // プロフィール画像は任意
isActive: boolean;
};

// ユーザー情報を更新するAPIのペイロード型
// Partialを使うことで、更新したい項目だけを送ればOKになる
type UpdateUserProfilePayload = Partial;

// ユーザー情報更新の例
function updateUserProfile(userId: string, data: UpdateUserProfilePayload) {
console.log(`ユーザーID: ${userId} の情報を更新します。`, data);
// … 実際にAPIを呼び出す処理 …
}

// 名前の更新だけを行いたい場合
updateUserProfile(“user-123”, {
userName: “新しい名前”,
});

// メールアドレスとプロフィール画像の更新を行いたい場合
updateUserProfile(“user-123”, {
email: “new_email@example.com”,
profileImage: “new_image_url.jpg”,
});

// isActiveをfalseにしたい場合
updateUserProfile(“user-123”, {
isActive: false,
});

どうですか? `Partial`を使うことで、`updateUserProfile`関数に渡す`data`オブジェクトが、`UserProfile`型のプロパティを「どれでも好きなだけ、一つからでも」含めることができるようになりました。これにより、APIの柔軟性が格段に向上し、コードもスッキリします。

そして、`-?`(`Required`)の方は、例えば、一度オプショナルなデータを受け取った後で、「この段階では、このデータは必須になっているはずだ!」と型を保証したい場合などに使えます。

あるいは、特定のプロパティだけを必須に戻す、といったカスタムなMapped Typeを作ることも可能です。

// Partialをベースに、特定のプロパティだけを必須に戻すカスタム型
type UpdateUserProfileWithRequiredName = Partial &
Required>;

/
この型は、以下のような意味になります
type UpdateUserProfileWithRequiredName = {
userId?: string;
userName: string; // ここが必須に戻った!
email?: string;
birthDate?: string;
profileImage?: string;
isActive?: boolean;
};
/

// userNameは必須なので、これを欠くとエラーになる
const validUpdate: UpdateUserProfileWithRequiredName = {
userName: “田中 太郎”, // userNameは必須
email: “tanaka@example.com”, // 他はオプショナル
};

// エラー: ‘userName’ プロパティが型 ‘{ email: string; }’ にありません。
/
const invalidUpdate: UpdateUserProfileWithRequiredName = {
email: “invalid@example.com”,
};
/

console.log(validUpdate); // { userName: ‘田中 太郎’, email: ‘tanaka@example.com’ }

少し複雑に見えるかもしれませんが、`&`(インターセクション型)や`Pick`(特定のプロパティだけを抜き出すユーティリティ型)と組み合わせることで、TypeScriptの型システムは無限の表現力を持ちます。現場では、このような柔軟な型定義が、コードの堅牢性と開発効率を大きく向上させるんです。

まとめ: TypeScriptの型は「設計図の柔軟性」

今日の話をまとめましょう。

  • Mapped Typesは、既存の型を元に新しい型を自動生成する「賢い型変換マシン」です。
  • `?`修飾子をプロパティに付与すると、そのプロパティは「オプショナル(任意)」になります。
  • `Partial`は、`T`型の全てのプロパティをオプショナルにするユーティリティ型です。
  • `-?`修飾子をプロパティに付与すると、もし`?`が付いていた場合はそれが削除され、そのプロパティは「必須」に戻ります。
  • `Required`は、`T`型の全てのプロパティを必須にするユーティリティ型です。

これらの機能は、特にAPIの設計や、コンポーネントのPropsの型定義など、柔軟なデータ構造を扱う場面で真価を発揮します。初めは少し呪文のように感じるかもしれませんが、これらの概念を理解し、実際に手を動かして試してみることで、TypeScriptの奥深さと便利さが実感できるはずです。

大丈夫、皆さんのTypeScriptの旅はまだ始まったばかり。一歩ずつ、着実に進んでいきましょう。この「設計図の柔軟性」をマスターすれば、あなたの書くコードは、もっと強く、もっとしなやかになりますよ!

それでは、また次の記事でお会いしましょう!

コメント

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