Mapped TypesとConditional Types
この章では、TypeScriptの高度な型機能であるMapped Types(マップ型)とConditional Types(条件付き型)について学びます。これらを理解すると、独自のユーティリティ型を作成できるようになります。
この章で学ぶこと
- Mapped Typesの基本と仕組み
- 修飾子の追加・削除(readonly、?)
- Conditional Typesの基本構文
- inferキーワードによる型推論
- Template Literal Types(テンプレートリテラル型)
- カスタムユーティリティ型の作成
応用編: この章の内容は高度なトピックです。日常的なTypeScript開発では必ずしも必要ではありませんが、ライブラリ開発や複雑な型を定義する際に役立ちます。初学者は一度読み飛ばし、他の章を学んでから戻ってきても構いません。
Mapped TypesとConditional Typesは、TypeScriptの組み込みユーティリティ型(Partial、Required、ReturnTypeなど)の実装に使われている技術です。これらを理解することで、より高度な型プログラミングが可能になります。
Mapped Typesとは
Mapped Typesは、既存の型から新しい型を生成する「型のfor文」のような機能です。オブジェクト型の各プロパティに対して変換を適用できます。
基本的なMapped Types
// 元の型
interface User {
id: number;
name: string;
email: string;
}
// すべてのプロパティをオプショナルにする
// [K in keyof User]でUserの各プロパティ名を反復
type PartialUser = {
[K in keyof User]?: User[K];
};
// 結果:
// {
// id?: number;
// name?: string;
// email?: string;
// }
// すべてのプロパティを読み取り専用にする
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// 結果:
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
構文の解説:
[K in keyof User]: Userのプロパティ名を順番に取り出す(K = "id", "name", "email")User[K]: プロパティKに対応する型を取得(User["id"] = number など)?: オプショナル修飾子を追加readonly: 読み取り専用修飾子を追加
ジェネリックなMapped Types
// 汎用的なPartialを自作
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 汎用的なReadonlyを自作
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 使用例
interface Product {
id: number;
name: string;
price: number;
}
type PartialProduct = MyPartial<Product>;
// { id?: number; name?: string; price?: number; }
type ReadonlyProduct = MyReadonly<Product>;
// { readonly id: number; readonly name: string; readonly price: number; }
修飾子の追加と削除
Mapped Typesでは、-(マイナス)記号を使って修飾子を削除できます。
readonlyの削除
interface ReadonlyUser {
readonly id: number;
readonly name: string;
readonly email: string;
}
// readonlyを削除する型
// -readonlyでreadonly修飾子を削除
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type MutableUser = Mutable<ReadonlyUser>;
// 結果:
// {
// id: number; // readonlyが削除された
// name: string;
// email: string;
// }
オプショナルの削除
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
// オプショナル(?)を削除する型
// -?でオプショナル修飾子を削除
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
type ConcreteUser = Concrete<PartialUser>;
// 結果:
// {
// id: number; // ?が削除された
// name: string;
// email: string;
// }
両方の修飾子を削除
interface User {
readonly id?: number;
readonly name?: string;
readonly email?: string;
}
// readonlyと?の両方を削除
type MutableConcrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
type FullUser = MutableConcrete<User>;
// 結果:
// {
// id: number;
// name: string;
// email: string;
// }
型の値の変換
プロパティの型自体も変換できます。
interface User {
id: number;
name: string;
email: string;
}
// すべてのプロパティをboolean型に変換
type Booleanify<T> = {
[K in keyof T]: boolean;
};
type UserFlags = Booleanify<User>;
// 結果:
// {
// id: boolean;
// name: boolean;
// email: boolean;
// }
// すべてのプロパティをPromiseでラップ
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
type AsyncUser = Promisify<User>;
// 結果:
// {
// id: Promise<number>;
// name: Promise<string>;
// email: Promise<string>;
// }
// すべてのプロパティをnull許容に
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
// 結果:
// {
// id: number | null;
// name: string | null;
// email: string | null;
// }
Conditional Typesとは
Conditional Types(条件付き型)は「型のif文」です。条件に基づいて異なる型を返します。
基本構文
// 基本構文: T extends U ? X : Y
// Tが型Uに代入可能ならX、そうでなければY
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<"hello">; // true(リテラル型もstring)
実用例:配列かどうか判定
type IsArray<T> = T extends any[] ? true : false;
type Test1 = IsArray<number[]>; // true
type Test2 = IsArray<string>; // false
type Test3 = IsArray<[1, 2, 3]>; // true(タプルも配列)
型に応じた変換
// 配列なら要素の型、そうでなければそのまま
type Flatten<T> = T extends any[] ? T[number] : T;
type Test1 = Flatten<number[]>; // number
type Test2 = Flatten<string[]>; // string
type Test3 = Flatten<boolean>; // boolean
// 使用例
function flatten<T>(value: T): Flatten<T> {
if (Array.isArray(value)) {
return value[0] as Flatten<T>;
}
return value as Flatten<T>;
}
inferキーワード
inferは、Conditional Typesの中で型を推論・抽出するためのキーワードです。
配列の要素型を取得
// 配列の要素型を抽出
// infer Eで配列の要素型をEとして推論
type ElementType<T> = T extends (infer E)[] ? E : T;
type Test1 = ElementType<number[]>; // number
type Test2 = ElementType<string[]>; // string
type Test3 = ElementType<boolean>; // boolean(配列でない場合はそのまま)
関数の戻り値型を取得(ReturnTypeの実装)
// ReturnTypeの自作版
// (...args: any[]) => infer R で戻り値の型をRとして推論
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function createUser() {
return { id: 1, name: "山田太郎" };
}
type User = MyReturnType<typeof createUser>;
// 結果: { id: number; name: string; }
関数の引数型を取得(Parametersの実装)
// Parametersの自作版
// (...args: infer P) で引数の型をPとして推論
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
function update(id: number, name: string, active: boolean): void {
console.log(id, name, active);
}
type UpdateParams = MyParameters<typeof update>;
// 結果: [number, string, boolean]
Promiseの値の型を取得
// Promiseの中身を取り出す
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Test1 = UnwrapPromise<Promise<string>>; // string
type Test2 = UnwrapPromise<Promise<number>>; // number
type Test3 = UnwrapPromise<boolean>; // boolean
// 深くネストしたPromiseも再帰的に展開
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U>
: T;
type Test4 = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>;
// 結果: string
Distributive Conditional Types
Union型に対してConditional Typesを適用すると、各要素に「分配」されます。
// 各要素を配列にラップ
type ToArray<T> = T extends any ? T[] : never;
type Test1 = ToArray<string | number>;
// 結果: string[] | number[](各要素に分配される)
// 分配を防ぐには[]で囲む
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Test2 = ToArrayNonDistributive<string | number>;
// 結果: (string | number)[](Union型全体が1つの配列に)
なぜ分配されるのか
Distribution は、「裸の型パラメータ(T が extends 左辺にそのまま現れる)」にのみ起こる仕様です。Union に対して条件型を適用すると、次のように展開されます。
// T = string | number のとき、ToArray<T> は以下のように展開される
ToArray<string | number>
= (string extends any ? string[] : never) | (number extends any ? number[] : never)
= string[] | number[]
つまり、Union の各メンバーごとに条件型が評価され、その結果を Union で結合する、という仕組みです。これが「分配(distribution)」です。
分配を抑制したい場合は、型パラメータを [T] のように 1 要素タプルで囲むのが定石です。タプルで囲むと T は「裸の型パラメータ」ではなくなり、Union 全体が 1 つの型として扱われます。
型パラメータが裸かどうかの見分け方
| パターン | 分配される? |
|---|---|
T extends U ? X : Y | ✅ される |
[T] extends [U] ? X : Y | ❌ されない |
{ a: T } extends { a: U } ? X : Y | ❌ されない |
T & {} extends U ? X : Y | ❌ されない |
実用例:NonNullableの実装
// null/undefinedを除外(分配が活用される)
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Test = MyNonNullable<string | null | undefined>;
// 分配の動作:
// MyNonNullable<string> | MyNonNullable<null> | MyNonNullable<undefined>
// = string | never | never
// = string
Template Literal Types
文字列リテラル型をテンプレート構文で組み合わせる機能です。
基本的な使い方
type World = "world";
type Greeting = `hello ${World}`;
// 結果: "hello world"
// Union型との組み合わせ
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type ColorSize = `${Color}-${Size}`;
// 結果: "red-small" | "red-medium" | "red-large" |
// "blue-small" | "blue-medium" | "blue-large" |
// "green-small" | "green-medium" | "green-large"
組み込みの文字列操作型
// 大文字に変換
type Upper = Uppercase<"hello">; // "HELLO"
// 小文字に変換
type Lower = Lowercase<"HELLO">; // "hello"
// 最初の文字を大文字に
type Cap = Capitalize<"hello">; // "Hello"
// 最初の文字を小文字に
type Uncap = Uncapitalize<"Hello">; // "hello"
実用例:イベントハンドラ型
type EventName = "click" | "focus" | "blur";
// on + 頭文字大文字のイベントハンドラ名を生成
type EventHandler = `on${Capitalize<EventName>}`;
// 結果: "onClick" | "onFocus" | "onBlur"
// イベントハンドラオブジェクトの型を生成
type EventHandlers = {
[K in EventName as `on${Capitalize<K>}`]: (event: Event) => void;
};
// 結果:
// {
// onClick: (event: Event) => void;
// onFocus: (event: Event) => void;
// onBlur: (event: Event) => void;
// }
const handlers: EventHandlers = {
onClick: (e) => console.log("Clicked!"),
onFocus: (e) => console.log("Focused!"),
onBlur: (e) => console.log("Blurred!")
};
実用例:CSSカスタムプロパティ
type CSSProperty = "color" | "background" | "border";
// CSSカスタムプロパティ名を生成
type CSSVar = `--${CSSProperty}`;
// 結果: "--color" | "--background" | "--border"
// getComputedStyleの型安全なラッパー
function getCSSVar(element: Element, property: CSSVar): string {
return getComputedStyle(element).getPropertyValue(property);
}
infer × Template Literal Types
infer と Template Literal Types を組み合わせると、文字列の中から一部を抽出して型として取り出すことができます。これは型レベルで正規表現のような処理を行うパターンです。
ルートパスからパラメータ名を抽出
// "/users/:id/posts/:postId" のような URL テンプレートから
// パラメータ名(":id" / ":postId")を Union 型として抽出する型
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never
type UserPostParams = ExtractParams<'/users/:id/posts/:postId'>
// 結果: 'id' | 'postId'
// パラメータを受け取る型安全なビルダー
type ParamsObject<Path extends string> = {
[K in ExtractParams<Path>]: string
}
function buildPath<Path extends string>(path: Path, params: ParamsObject<Path>): string {
return path.replace(/:(\w+)/g, (_, key) => (params as Record<string, string>)[key])
}
// 型安全にパラメータを渡せる
buildPath('/users/:id/posts/:postId', { id: '1', postId: '42' })
// ✅ OK
buildPath('/users/:id/posts/:postId', { id: '1' })
// ❌ Error: postId が不足
スネークケース → キャメルケース変換
type SnakeToCamel<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
: S
type Result1 = SnakeToCamel<'hello_world'> // 'helloWorld'
type Result2 = SnakeToCamel<'user_first_name'> // 'userFirstName'
このパターンは、API レスポンス(スネークケース)をフロントエンド(キャメルケース)の型に変換する際などに実用的です。再帰的な条件型と Template Literal Types の組み合わせで、ランタイムコードを書かずに型変換を行えます。
TypeScript の型システムは再帰の深さに上限があります(デフォルトで 50 段階)。過度に深いネスト構造に対する再帰型はエラーになるため、複雑な文字列処理はランタイム関数と型レベル処理を使い分けることが重要です。
カスタムユーティリティ型の作成
学んだ技術を組み合わせて、独自のユーティリティ型を作成できます。
Getterを生成する型
// プロパティ名からgetter名を生成
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// 結果:
// {
// getName: () => string;
// getAge: () => number;
// }
特定の型のプロパティだけを抽出
// 値がstring型のプロパティだけを抽出
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserStrings = StringProperties<User>;
// 結果:
// {
// name: string;
// email: string;
// }
関数プロパティだけを抽出
// 値が関数型のプロパティだけを抽出
type FunctionProperties<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};
interface Service {
name: string;
version: number;
start: () => void;
stop: () => void;
getStatus: () => string;
}
type ServiceMethods = FunctionProperties<Service>;
// 結果:
// {
// start: () => void;
// stop: () => void;
// getStatus: () => string;
// }
試してみよう: カスタムユーティリティ型を作ろう ★★★
以下の要件を満たすカスタムユーティリティ型を実装してください。
要件:
DeepReadonly<T>: ネストしたオブジェクトも含めて再帰的に読み取り専用にするOptionalExcept<T, K>: 指定したプロパティK以外をオプショナルにするAsyncMethods<T>: すべてのメソッドの戻り値をPromiseでラップする
// 以下を実装してください
// type DeepReadonly<T> = ...
// type OptionalExcept<T, K> = ...
// type AsyncMethods<T> = ...
ヒント
DeepReadonly: 再帰的にMapped Typesを適用。オブジェクトかどうかをConditional Typesで判定OptionalExcept: PickとPartialを組み合わせ、指定キー以外にPartialを適用して交差型にAsyncMethods: 関数型かどうかを判定し、戻り値をPromiseでラップ
回答と解説
// 1. DeepReadonly: 再帰的に読み取り専用にする
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K] // 関数はそのまま
: DeepReadonly<T[K]> // オブジェクトは再帰
: T[K]; // プリミティブはそのまま
};
// テスト
interface NestedConfig {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
timeout: number;
}
type ReadonlyConfig = DeepReadonly<NestedConfig>;
// すべてのネストしたプロパティがreadonlyになる
const config: ReadonlyConfig = {
server: {
host: "localhost",
port: 3000,
ssl: {
enabled: true,
cert: "cert.pem"
}
},
timeout: 5000
};
// config.server.ssl.enabled = false; // Error: 読み取り専用
// 2. OptionalExcept: 指定したプロパティ以外をオプショナルに
type OptionalExcept<T, K extends keyof T> =
Pick<T, K> & Partial<Omit<T, K>>;
// テスト
interface User {
id: number;
name: string;
email: string;
age: number;
}
// idとnameは必須、それ以外はオプショナル
type UserWithRequiredIdName = OptionalExcept<User, "id" | "name">;
const user1: UserWithRequiredIdName = {
id: 1,
name: "山田太郎"
// email, ageは省略可能
};
const user2: UserWithRequiredIdName = {
id: 2,
name: "田中花子",
email: "tanaka@example.com" // 指定してもOK
};
// 3. AsyncMethods: メソッドの戻り値をPromiseでラップ
type AsyncMethods<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R> // 関数なら戻り値をPromiseに
: T[K]; // 関数でなければそのまま
};
// テスト
interface UserService {
name: string;
getUser(id: number): User;
saveUser(user: User): boolean;
deleteUser(id: number): void;
}
type AsyncUserService = AsyncMethods<UserService>;
// 結果:
// {
// name: string; // 関数でないのでそのまま
// getUser(id: number): Promise<User>;
// saveUser(user: User): Promise<boolean>;
// deleteUser(id: number): Promise<void>;
// }
// 実装例
const asyncService: AsyncUserService = {
name: "UserService",
async getUser(id: number) {
return { id, name: "User", email: "user@example.com", age: 20 };
},
async saveUser(user: User) {
console.log("Saving:", user);
return true;
},
async deleteUser(id: number) {
console.log("Deleting:", id);
}
};
解説:
DeepReadonly: Conditional TypesでobjectかつFunctionでないものを再帰的に処理OptionalExcept:Pick<T, K>で必須プロパティを取り出し、Partial<Omit<T, K>>で残りをオプショナルに、交差型で合成AsyncMethods:inferでメソッドの引数と戻り値の型を抽出し、戻り値をPromiseでラップ
これらのパターンを理解すれば、プロジェクト固有のユーティリティ型を自由に作成できます。
まとめ
この章で学んだこと:
- Mapped Types: 既存の型から新しい型を生成する「型のfor文」
- 修飾子の操作:
-readonlyや-?で修飾子を削除 - Conditional Types:
T extends U ? X : Yによる「型のif文」 - inferキーワード: 型の一部を推論・抽出
- Distributive Conditional Types: Union型への分配動作
- Template Literal Types: 文字列リテラル型のテンプレート合成
- 組み込み文字列操作型:
Uppercase、Lowercase、Capitalize、Uncapitalize
これらの高度な型機能を組み合わせることで、TypeScriptの組み込みユーティリティ型と同様の型や、プロジェクト固有のカスタム型を作成できます。
初学者がつまずきやすいポイント
よくある間違い
❌ Mapped Typesでkeyofを忘れる
// ❌ keyofがない
type BadPartial<T> = {
[K in T]?: T[K]; // Error: Tはキーとして使えない
};
// ✅ keyofでキーを取得
type GoodPartial<T> = {
[K in keyof T]?: T[K];
};
原因: [K in T]では、Tがキーの集合である必要がありますが、オブジェクト型はキーの集合ではありません。
解決策: keyof Tでキーの集合(Union型)を取得してからMapped Typesで反復しましょう。
❌ Conditional Typesの分配を意識しない
type ToArray<T> = T extends any ? T[] : never;
// Union型に対して分配される
type Result = ToArray<string | number>;
// 結果: string[] | number[] ((string | number)[] ではない)
// ❌ 分配を期待せずに使うと予期しない結果に
原因: 裸の型パラメータにConditional Typesを適用すると、Union型の各メンバーに分配されます。
解決策: 分配を防ぎたい場合は[T] extends [any]のようにブラケットで囲みましょう。
❌ Template Literal Typesで変数を使おうとする
// ❌ ランタイムの変数は型に使えない
const prefix = "on";
// type Handler = `${prefix}Click`; // Error
// ✅ リテラル型を使う
type Prefix = "on";
type Handler = `${Prefix}Click`; // "onClick"
原因: Template Literal Typesはコンパイル時に解決される型であり、ランタイムの変数は使えません。
解決策: as constでリテラル型にするか、型として直接定義しましょう。
❌ inferの位置を間違える
// ❌ inferが条件の外にある
// type Bad<T> = infer R extends T ? R : never; // Error
// ✅ inferは条件の中でのみ使用可能
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
原因: inferはConditional Typesのextends句の中でのみ使用できます。
解決策: T extends ... infer R ... ? ... : ...の形式で使用しましょう。
❌ 再帰型で終了条件を忘れる
// ❌ 終了条件がないと無限ループ
// type BadFlatten<T> = T extends any[] ? BadFlatten<T[number]> : T;
// ✅ 終了条件を設定
type Flatten<T> = T extends any[]
? T[number] extends any[]
? Flatten<T[number]>
: T[number]
: T;
原因: 再帰型には終了条件が必要です。 解決策: Conditional Typesで基底ケース(これ以上再帰しない条件)を定義しましょう。
次に読む
開発環境 へ進みます。モジュール、tsconfig、開発ツールチェーンの構築を学びます。