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

おい、諸君! TypeScript の世界へようこそ。私は長年フロントエンドの最前線で戦ってきたアーキテクトだ。今日は、日々の開発で必ずお世話になるであろう「Mapped Types」のちょっとした、でもめちゃくちゃ便利なテクニックに焦点を当てる。具体的には、プロパティに `?` を付けたり外したりする方法だ。

「え、そんなの基本じゃない?」と思った君。甘い! 実務では、既存の型をちょっとだけ変えたい、でも元の型を壊したくない、なんて場面が腐るほど出てくるんだ。そんな時、この `?` の扱いをマスターしているかどうかで、コードの品質と開発効率がまるで変わってくる。

ブラウザが裏側でどう動いてるか、なんて話は、正直なところフロントエンドエンジニアにとってはあまり関係ない。大事なのは、TypeScript がどうやって我々のコードを安全で、かつ柔軟にするかだ。今回は、その「柔軟性」の鍵となる `?` 修飾子の操作に、ガッツリと切り込んでいこう。

Mapped Typesで `?` を操る:オプショナルを制御する鉄則

Mapped Types とは、既存の型から新しい型を作り出す強力な機能だ。例えば、あるオブジェクトの全プロパティを読み取り専用にしたり、あるいは今回のように、オプショナル(任意)にしたり、逆に必須にしたりできる。

1. プロパティをオプショナルにする (`?` の付与)

まずは基本中の基本。既存の型 `T` の各プロパティに `?` を付けて、すべてオプショナルな型 `Partial` を作る方法から始めよう。これは `Partial` というユーティリティ型として標準で用意されているから、みんなもよく目にしているはずだ。

// 元となる型定義
interface UserProfile {
id: number;
name: string;
email: string;
age?: number; // 元々オプショナルなプロパティもそのままオプショナルに保たれる
}

// Mapped Type を使って、全プロパティをオプショナルにする
// T[K] はプロパティの値の型、K はプロパティ名
// `?` を付けることで、そのプロパティはオプショナルになる
type PartialUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};

// 実際に使ってみよう
const partialUser: PartialUserProfile = {
id: 1, // オプショナルなので指定しなくても良いが、指定してもOK
name: “Alice”,
// email はオプショナルになった
// age は元々オプショナルだったので、そのままオプショナル
};

console.log(partialUser);

どうだ? `PartialUserProfile` という型を見ると、`id` も `name` も、そして `email` も、すべて `?` が付いているのがわかるだろう。元々 `age` に `?` が付いていたとしても、`Partial` 化すれば、そのまま `?` が維持される。これが Mapped Types の力だ。

2. プロパティを必須にする (`-?` による `?` の削除)

さて、ここからが本題。もし、ある型定義があって、その型定義を元に「すべてのプロパティを必須にしたい」となったらどうするか? ここで登場するのが、Mapped Types の修飾子操作だ。

Mapped Types では、`+` や `-` を使って、プロパティ修飾子(`readonly` や `?`)を付けたり外したりできる。プロパティを必須にするには、`?` を削除する、つまり `-?` を使うんだ。

// 元となる型定義(一部オプショナルなプロパティを含む)
interface ProductInfo {
productId: string;
productName: string;
price: number;
description?: string; // オプショナルなプロパティ
stockCount?: number; // オプショナルなプロパティ
}

// Mapped Type を使って、全プロパティを必須にする
// `-?` を付けることで、元の型で `?` が付いていたプロパティも必須になる
type RequiredProductInfo = {
[K in keyof ProductInfo]-?: ProductInfo[K];
};

// 実際に使ってみよう
// 全てのプロパティを指定する必要がある
const requiredProduct: RequiredProductInfo = {
productId: “XYZ123”,
productName: “Awesome Gadget”,
price: 99.99,
description: “A really awesome gadget.”, // オプショナルだったが必須になった
stockCount: 100, // オプショナルだったが必須になった
};

console.log(requiredProduct);

// もし一部のプロパティを省略すると、TypeScript がエラーを返してくれる
/
const incompleteProduct: RequiredProductInfo = {
productId: “ABC456”,
productName: “Another Gadget”,
price: 50.00,
// description がない! error TS2741: Property ‘description’ is missing in type ‘{ productId: string; productName: string; price: number; }’ but required in type ‘RequiredProductInfo’.
// stockCount もない! error TS2741: Property ‘stockCount’ is missing in type ‘{ productId: string; productName: string; price: number; }’ but required in type ‘RequiredProductInfo’.
};
/

どうだろうか、この `-?` の威力は。`ProductInfo` 型では `description` や `stockCount` はオプショナルだったが、`RequiredProductInfo` 型では、これらもすべて必須になっている。これは、例えばAPIから受け取ったデータはオプショナルだけど、それを加工して保存する際は必須にしたい、といったシナリオで非常に役立つ。

3. 部分的な `?` の制御: `Pick` と `Omit` と組み合わせる

「いや、全部じゃなくて、特定のプロパティだけ `?` を付けたり外したりしたいんだけど?」という要望も当然出てくる。そんな時は、`Pick` や `Omit` といったユーティリティ型と組み合わせることで、より柔軟な型定義が可能になる。

例えば、`UserProfile` 型の `email` だけをオプショナルにしたくない場合。

interface UserProfile {
id: number;
name: string;
email?: string; // 元々オプショナル
age?: number;
}

// email を必須にしつつ、他のオプショナルなプロパティはそのままにする
type UserProfileWithRequiredEmail =
// まず、email 以外のプロパティを Pick で抜き出す
// そして、それらを Mapped Type で再構築する。ここで `-?` を使って必須にする
{
[K in Exclude]-?: UserProfile[K];
}
// email プロパティは、元の型からそのまま取る(オプショナルならオプショナル、必須なら必須)
& Pick;

// 別の方法:email を除いた型を `Required` にして、email を追加する
// (ただし、email が元々オプショナルだった場合、これだと必須になる)
// type UserProfileWithRequiredEmail_alternative = Required> & Pick;

// 実際に使ってみよう
const user1: UserProfileWithRequiredEmail = {
id: 1,
name: “Bob”,
email: “bob@example.com”, // 必須なので指定が必要
age: 30,
};

console.log(user1);

/
const user2: UserProfileWithRequiredEmail = {
id: 2,
name: “Charlie”,
// email がない! error TS2741: Property ‘email’ is missing in type ‘{ id: number; name: string; }’ but required in type ‘{ email: string; }’.
age: 25,
};
/

// 元の UserProfile 型なら、email はオプショナルで良い
const user3: UserProfile = {
id: 3,
name: “David”,
age: 40,
};
console.log(user3);

この例では、`Exclude` で `email` 以外のキーを取得し、それらを `-?` を使って必須にし、最後に `Pick` で `email` プロパティを(元の型定義のまま)追加している。これにより、「`email` 以外は必須、`email` は元の定義通り(ここではオプショナル)」という、非常に細かい制御が可能になる。

逆に、特定のプロパティだけオプショナルにしたい場合も、同様の考え方で実現できる。

まとめ: `?` と `-?` を使いこなせ!

Mapped Types における `?` と `-?` の操作は、TypeScript の型システムをより柔軟に、より強力に使うための必須スキルだ。

  • `{ [K in keyof T]?: T[K] }` : `T` の全プロパティをオプショナルにする (`Partial` と同じ)
  • `{ [K in keyof T]-?: T[K] }` : `T` の全プロパティを必須にする (`Required` と同じ)
  • `Pick`, `Omit`, `Exclude`, `Extract` などを組み合わせることで、特定のプロパティだけを対象にした `?` の付与・削除も自在にできる。

これらのテクニックを使いこなせば、コードの保守性も、再利用性も格段に向上する。型定義は単なる「おまじない」ではない。それは、我々開発者が未来の自分やチームメンバーとコミュニケーションを取るための「設計図」であり、「契約書」なんだ。

今日の話が、君たちの TypeScript マスターへの道をさらに一歩進める助けになれば幸いだ。もし質問があれば、いつでも声をかけてくれ。現場で待ってるぞ!

コメント

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