TypeScriptを使い始めると、「既存の型をベースに少しだけ変更した型を作りたい」「条件によって異なる型を返したい」という場面に頻繁に遭遇します。このような要件を手作業で実装すると、型定義の重複やメンテナンスコストの増大を招きます。
本記事では、TypeScriptの強力な型操作機能であるMapped Types(マップ型)とConditional Types(条件付き型)を徹底解説します。これらの機能を習得することで、既存の型から動的に新しい型を生成する型レベルのメタプログラミングが可能になります。
この記事で学べること#
- Mapped Typesの基本構文と動作原理
- キー修飾子(
readonly、?)の追加・削除
as句によるキーのリマッピング
- Conditional Typesの基本と
extendsキーワードの使い方
inferキーワードによる型の抽出
- Template Literal Typesを活用した文字列型の操作
- 実務で役立つMapped TypesとConditional Typesの組み合わせパターン
前提条件#
前提知識#
本記事は、以下の知識を前提としています。
動作確認環境#
| ツール |
バージョン |
| Node.js |
20.x以上 |
| TypeScript |
5.7以上 |
| VS Code |
最新版 |
本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。
期待される結果#
本記事のコードを実行すると、以下の動作を確認できます。
- Mapped Typesが既存の型からプロパティを変換する
- Conditional Typesが入力型に応じて適切な型を返す
inferキーワードが関数の戻り値型や配列の要素型を抽出する
Mapped Typesとは#
Mapped Types(マップ型)は、既存の型のプロパティを反復処理して、新しい型を生成する仕組みです。JavaScriptのArray.prototype.map()が配列の各要素を変換するように、Mapped Typesは型のプロパティを変換します。
基本構文#
Mapped Typesの基本構文は以下のとおりです。
1
2
3
|
type MappedType<T> = {
[P in keyof T]: T[P];
};
|
この構文の各要素を解説します。
P in keyof T: 型Tのすべてのプロパティキーを反復処理
T[P]: インデックスアクセス型で、プロパティPの型を取得
以下は、すべてのプロパティをboolean型に変換するMapped Typeの例です。
1
2
3
4
5
6
7
8
9
10
11
12
|
type OptionsFlags<T> = {
[P in keyof T]: boolean;
};
interface Features {
darkMode: () => void;
newUserProfile: () => void;
}
// 各プロパティがbooleanに変換される
type FeatureOptions = OptionsFlags<Features>;
// 結果: { darkMode: boolean; newUserProfile: boolean }
|
Mapped Typesの動作原理#
Mapped Typesの動作を図で表すと以下のようになります。
flowchart LR
subgraph 入力型
A["{<br/>name: string<br/>age: number<br/>}"]
end
subgraph Mapped Type処理
B["[P in keyof T]"]
C["変換ルール適用"]
end
subgraph 出力型
D["{<br/>name: NewType<br/>age: NewType<br/>}"]
end
A --> B --> C --> Dkeyof Tは型Tのすべてのプロパティキーをユニオン型として取得します。inキーワードはそのユニオンを反復処理し、各キーに対して右辺の型を適用します。
キー修飾子の操作#
Mapped Typesでは、readonlyと?(オプショナル)という2つの修飾子を操作できます。これにより、既存の型のプロパティに対して読み取り専用やオプショナルを追加・削除できます。
修飾子の追加#
readonlyや?を付与することで、すべてのプロパティに修飾子を追加できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
interface User {
id: number;
name: string;
email: string;
}
// すべてのプロパティをreadonlyにする
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
// 結果: { readonly id: number; readonly name: string; readonly email: string }
// すべてのプロパティをオプショナルにする
type PartialUser = {
[P in keyof User]?: User[P];
};
// 結果: { id?: number; name?: string; email?: string }
|
修飾子の削除#
-プレフィックスを使用すると、修飾子を削除できます。逆に+プレフィックスは明示的に追加を意味しますが、省略した場合と同じ動作です。
1
2
3
4
5
6
7
8
9
10
|
interface LockedAccount {
readonly id: string;
readonly name: string;
}
// readonlyを削除して変更可能にする
type UnlockedAccount = {
-readonly [P in keyof LockedAccount]: LockedAccount[P];
};
// 結果: { id: string; name: string }
|
以下は、オプショナルを削除して必須にする例です。
1
2
3
4
5
6
7
8
9
10
11
|
interface MaybeUser {
id: string;
name?: string;
age?: number;
}
// オプショナルを削除して必須プロパティにする
type RequiredUser = {
[P in keyof MaybeUser]-?: MaybeUser[P];
};
// 結果: { id: string; name: string; age: number }
|
修飾子操作の実践例#
実務では、フォームの入力値を扱う際に修飾子操作が役立ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// データベースから取得したユーザー型(すべて必須)
interface UserEntity {
id: number;
username: string;
email: string;
createdAt: Date;
}
// 更新フォーム用の型(id以外はオプショナル)
type UserUpdateForm = {
[P in keyof UserEntity]?: UserEntity[P];
} & { id: number };
// idは必須、その他はオプショナル
// 登録フォーム用の型(idとcreatedAtは不要)
type UserCreateForm = Omit<{
[P in keyof UserEntity]: UserEntity[P];
}, 'id' | 'createdAt'>;
// 結果: { username: string; email: string }
|
asによるキーのリマッピング#
TypeScript 4.1以降では、as句を使用してプロパティキーを変換できます。この機能により、元の型のキーを新しいキーに変換したり、特定のキーをフィルタリングしたりできます。
基本的なキーのリマッピング#
as句の基本構文は以下のとおりです。
1
2
3
|
type MappedWithRemap<T> = {
[P in keyof T as NewKeyType]: T[P];
};
|
以下は、プロパティ名にgetプレフィックスを付けてゲッター関数型に変換する例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// 結果:
// {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
|
Capitalize<string & P>は、プロパティ名の先頭を大文字に変換する組み込みユーティリティ型です。string & Pは、Pがstring型であることを保証するためのインターセクション型です。
キーのフィルタリング#
as句でneverを返すと、そのプロパティは結果から除外されます。これにより、特定の条件に合致するプロパティのみを抽出できます。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 特定のキーを除外する
type RemoveKindField<T> = {
[P in keyof T as Exclude<P, 'kind'>]: T[P];
};
interface Circle {
kind: 'circle';
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// 結果: { radius: number }
|
以下は、関数型のプロパティのみを抽出する例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
type FunctionProperties<T> = {
[P in keyof T as T[P] extends Function ? P : never]: T[P];
};
interface Mixed {
name: string;
age: number;
greet: () => void;
calculate: (x: number) => number;
}
type OnlyFunctions = FunctionProperties<Mixed>;
// 結果: { greet: () => void; calculate: (x: number) => number }
|
イベントハンドラの型生成#
キーリマッピングの実践的な活用例として、イベントハンドラの型を自動生成するパターンを紹介します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type EventHandlers<T> = {
[P in keyof T as `on${Capitalize<string & P>}Changed`]: (
oldValue: T[P],
newValue: T[P]
) => void;
};
interface State {
count: number;
message: string;
isActive: boolean;
}
type StateHandlers = EventHandlers<State>;
// 結果:
// {
// onCountChanged: (oldValue: number, newValue: number) => void;
// onMessageChanged: (oldValue: string, newValue: string) => void;
// onIsActiveChanged: (oldValue: boolean, newValue: boolean) => void;
// }
|
Conditional Typesとは#
Conditional Types(条件付き型)は、型レベルの条件分岐を実現する機能です。入力された型に応じて、異なる型を返すことができます。
基本構文#
Conditional Typesの構文は、JavaScriptの三項演算子に似ています。
1
|
type ConditionalType<T> = T extends SomeType ? TrueType : FalseType;
|
この構文の意味は以下のとおりです。
T extends SomeType: TがSomeTypeに代入可能かどうかをチェック
TrueType: 条件が真の場合に返す型
FalseType: 条件が偽の場合に返す型
以下は、基本的な使用例です。
1
2
3
4
5
|
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true(リテラル型もstringに代入可能)
|
条件付き型の実践例#
実務では、関数のオーバーロードを条件付き型で表現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
// 入力が数値ならIdLabel、文字列ならNameLabelを返す
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw 'unimplemented';
}
const a = createLabel('typescript'); // NameLabel
const b = createLabel(42); // IdLabel
const c = createLabel(Math.random() > 0.5 ? 'hello' : 42);
// IdLabel | NameLabel
|
Conditional Typesの制約#
Conditional Typesでは、extendsによる条件チェックと同時に、条件が真の場合に型が絞り込まれます。これにより、より詳細な型情報にアクセスできます。
制約による型の絞り込み#
以下の例では、Tがmessageプロパティを持つ型かどうかをチェックし、持つ場合はその型を返します。
1
2
3
4
5
6
7
8
9
10
11
12
|
type MessageOf<T> = T extends { message: unknown } ? T['message'] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessage = MessageOf<Email>; // string
type DogMessage = MessageOf<Dog>; // never
|
配列要素の型を取得する#
Conditional Typesを使用して、配列の要素型を取得できます。
1
2
3
4
|
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number(配列でないのでそのまま)
|
T[number]は、配列型Tに対するインデックスアクセス型で、配列の要素型を取得します。
inferキーワードによる型推論#
inferキーワードは、Conditional Types内で型を抽出して変数のように扱う機能です。extendsの条件部分でinferを使用すると、パターンマッチングのように型の一部を抽出できます。
基本構文#
inferの基本的な使い方を見てみましょう。
1
2
3
4
5
|
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[]>; // number
type C = Flatten<boolean>; // boolean(配列でないのでそのまま)
|
infer Itemは、Array<...>の型パラメータ部分をItemとして抽出します。条件が真の場合、Itemをそのまま返します。
関数の戻り値型を取得する#
inferを使用して、関数の戻り値型を抽出できます。これは組み込みユーティリティ型ReturnTypeの実装と同等です。
1
2
3
4
5
6
7
|
type GetReturnType<T> = T extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<(x: string) => string>; // string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]
|
関数の引数型を取得する#
同様に、関数の引数型もタプルとして抽出できます。
1
2
3
4
5
6
7
|
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;
type Params1 = GetParameters<(x: number, y: string) => void>;
// [x: number, y: string]
type Params2 = GetParameters<() => void>;
// []
|
inferの実践的な活用例#
Promiseの中身の型を再帰的に取得するAwaited型は、inferの典型的な活用例です。
1
2
3
4
5
6
7
|
type MyAwaited<T> = T extends Promise<infer U>
? MyAwaited<U> // 再帰的にPromiseを剥がす
: T;
type A = MyAwaited<Promise<string>>; // string
type B = MyAwaited<Promise<Promise<number>>>; // number
type C = MyAwaited<string>; // string
|
以下は、オブジェクトの値の型のみを抽出する例です。
1
2
3
4
5
6
7
8
9
|
type ValueOf<T> = T extends { [key: string]: infer V } ? V : never;
interface Config {
host: string;
port: number;
debug: boolean;
}
type ConfigValue = ValueOf<Config>; // string | number | boolean
|
Distributive Conditional Types#
Conditional Typesがジェネリクスに対してユニオン型を受け取ると、**分配(Distribution)**が発生します。これは、ユニオンの各メンバーに対して条件付き型が適用されることを意味します。
分配の動作#
以下の例で、分配の動作を確認します。
1
2
3
4
5
|
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// 結果: string[] | number[]
// 期待通りに分配される
|
この動作を図で表すと以下のようになります。
flowchart TD
A["ToArray<string | number>"] --> B["分配処理"]
B --> C["ToArray<string>"]
B --> D["ToArray<number>"]
C --> E["string[]"]
D --> F["number[]"]
E --> G["string[] | number[]"]
F --> G分配を防ぐ方法#
分配を避けたい場合は、extendsの両側をタプルで囲みます。
1
2
3
4
5
|
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type StrOrNumArrayNonDist = ToArrayNonDist<string | number>;
// 結果: (string | number)[]
// 分配されず、ユニオンをそのまま配列にする
|
分配の実践的な活用#
分配の性質を利用して、ユニオン型から特定の型を除外できます。
1
2
3
4
5
6
7
|
type Exclude<T, U> = T extends U ? never : T;
type T0 = Exclude<'a' | 'b' | 'c', 'a'>;
// 結果: 'b' | 'c'
type T1 = Exclude<string | number | boolean, boolean>;
// 結果: string | number
|
逆に、特定の型のみを抽出することもできます。
1
2
3
4
5
6
7
|
type Extract<T, U> = T extends U ? T : never;
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>;
// 結果: 'a'
type T1 = Extract<string | number | boolean, number | boolean>;
// 結果: number | boolean
|
Template Literal Types#
Template Literal Types(テンプレートリテラル型)は、文字列リテラル型を動的に生成する機能です。JavaScriptのテンプレートリテラル構文と同様の記法を型レベルで使用できます。
基本構文#
Template Literal Typesは、バッククォートと${}を使用して文字列型を生成します。
1
2
|
type World = 'world';
type Greeting = `hello ${World}`; // "hello world"
|
ユニオン型を使用すると、すべての組み合わせが生成されます。
1
2
3
4
5
|
type EmailLocaleIDs = 'welcome_email' | 'email_heading';
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff';
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// 結果: "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
|
複数のユニオンを組み合わせる#
複数のユニオン型を組み合わせると、すべての組み合わせが直積として生成されます。
1
2
3
4
5
6
7
8
|
type Lang = 'en' | 'ja' | 'pt';
type AllLocaleIDs = 'welcome_email' | 'email_heading';
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// 結果:
// "en_welcome_email" | "en_email_heading" |
// "ja_welcome_email" | "ja_email_heading" |
// "pt_welcome_email" | "pt_email_heading"
|
組み込みの文字列操作型#
TypeScriptは、文字列を操作する組み込みユーティリティ型を提供しています。
| ユーティリティ型 |
説明 |
例 |
Uppercase<S> |
全文字を大文字に変換 |
Uppercase<'hello'> → 'HELLO' |
Lowercase<S> |
全文字を小文字に変換 |
Lowercase<'HELLO'> → 'hello' |
Capitalize<S> |
先頭文字を大文字に変換 |
Capitalize<'hello'> → 'Hello' |
Uncapitalize<S> |
先頭文字を小文字に変換 |
Uncapitalize<'HELLO'> → 'hELLO' |
以下は、これらのユーティリティ型の使用例です。
1
2
3
4
5
6
7
|
type HTTPMethod = 'get' | 'post' | 'put' | 'delete';
type UpperHTTPMethod = Uppercase<HTTPMethod>;
// 結果: "GET" | "POST" | "PUT" | "DELETE"
type CapitalizedMethod = Capitalize<HTTPMethod>;
// 結果: "Get" | "Post" | "Put" | "Delete"
|
イベント名の自動生成#
Template Literal Typesの典型的な活用例として、イベント名の自動生成があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
type PropEventSource<T> = {
on<K extends string & keyof T>(
eventName: `${K}Changed`,
callback: (newValue: T[K]) => void
): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
const person = makeWatchedObject({
firstName: 'Taro',
lastName: 'Yamada',
age: 26,
});
// 型安全なイベントリスナー
person.on('firstNameChanged', (newName) => {
// newNameはstring型と推論される
console.log(`firstName was changed to ${newName.toUpperCase()}`);
});
person.on('ageChanged', (newAge) => {
// newAgeはnumber型と推論される
if (newAge < 0) {
console.warn('warning! negative age');
}
});
// エラー: "firstName"はイベント名ではない
// person.on('firstName', () => {});
|
Mapped TypesとConditional Typesの組み合わせ#
Mapped TypesとConditional Typesを組み合わせることで、より高度な型変換が可能になります。
プロパティの型に基づくフィルタリング#
特定の型を持つプロパティのみを抽出する例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
type FilterByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P];
};
interface Mixed {
name: string;
age: number;
email: string;
isActive: boolean;
count: number;
}
type StringProps = FilterByType<Mixed, string>;
// 結果: { name: string; email: string }
type NumberProps = FilterByType<Mixed, number>;
// 結果: { age: number; count: number }
|
個人情報フラグに基づく型の生成#
実務で役立つ、PIIフラグに基づいてプロパティを分類する例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type ExtractPII<T> = {
[P in keyof T]: T[P] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: 'incrementing' };
name: { type: string; pii: true };
email: { type: string; pii: true };
createdAt: { type: Date };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
// 結果:
// {
// id: false;
// name: true;
// email: true;
// createdAt: false;
// }
|
DeepReadonly型の実装#
再帰的にすべてのプロパティを読み取り専用にする型の実装です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
interface Nested {
user: {
profile: {
name: string;
age: number;
};
settings: {
theme: string;
};
};
}
type ReadonlyNested = DeepReadonly<Nested>;
// すべてのプロパティが再帰的にreadonlyになる
|
パス型の生成#
ネストしたオブジェクトのパスを文字列リテラル型として生成する高度な例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
type PathKeys<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? PathKeys<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
: `${Prefix}${K}`;
}[keyof T & string]
: never;
interface Config {
database: {
host: string;
port: number;
};
cache: {
enabled: boolean;
ttl: number;
};
}
type ConfigPaths = PathKeys<Config>;
// 結果: "database" | "database.host" | "database.port" | "cache" | "cache.enabled" | "cache.ttl"
|
組み込みユーティリティ型の実装を理解する#
Mapped TypesとConditional Typesの知識があれば、TypeScriptの組み込みユーティリティ型の実装を理解できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
// Partialの実装
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// Requiredの実装
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
// Readonlyの実装
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// Pickの実装
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omitの実装
type MyOmit<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P];
};
// Recordの実装
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
// Excludeの実装
type MyExclude<T, U> = T extends U ? never : T;
// Extractの実装
type MyExtract<T, U> = T extends U ? T : never;
// NonNullableの実装
type MyNonNullable<T> = T extends null | undefined ? never : T;
// ReturnTypeの実装
type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
// Parametersの実装
type MyParameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
|
よくある間違いと対処法#
1. keyofの戻り値型の誤解#
keyofはstring | number | symbolのサブタイプを返します。Template Literal Typesで使用する場合は、string &でフィルタリングが必要です。
1
2
3
4
5
6
7
8
9
|
// 誤り: Pがsymbolの可能性があるためエラー
type Wrong<T> = {
[P in keyof T as `get${Capitalize<P>}`]: T[P];
};
// 正しい: string & Pでstringのキーのみに限定
type Correct<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: T[P];
};
|
2. Distributive Conditional Typesの意図しない分配#
ユニオン型を渡したときに分配を避けたい場合は、タプルで囲むことを忘れないでください。
1
2
3
4
5
6
7
|
// 分配される(意図しない場合がある)
type Distributed<T> = T extends any ? T[] : never;
type D = Distributed<string | number>; // string[] | number[]
// 分配されない
type NonDistributed<T> = [T] extends [any] ? T[] : never;
type ND = NonDistributed<string | number>; // (string | number)[]
|
3. inferの位置の誤り#
inferはextendsの右側でのみ使用できます。また、推論された型は条件が真の場合のみ使用可能です。
1
2
3
4
5
|
// 誤り: inferは条件の右側でのみ使用可能
// type Wrong<T> = infer U extends T ? U : never;
// 正しい
type Correct<T> = T extends Array<infer U> ? U : never;
|
まとめ#
本記事では、TypeScriptのMapped TypesとConditional Typesについて解説しました。これらの機能を使いこなすことで、型レベルのメタプログラミングが可能になり、型定義の重複を大幅に削減できます。
- Mapped Typesは既存の型のプロパティを反復処理して新しい型を生成する
- キー修飾子(
readonly、?)は+/-プレフィックスで追加・削除できる
- as句によるキーリマッピングでプロパティ名を変換できる
- Conditional Typesは型レベルの条件分岐を実現する
- inferキーワードでパターンマッチングのように型を抽出できる
- Template Literal Typesで文字列型を動的に生成できる
- これらを組み合わせることで、高度な型変換が可能になる
次のステップとして、型レベルプログラミング入門で、より高度な型操作テクニックを学ぶことをおすすめします。
参考リンク#