高度な型操作
この章では、TypeScriptの高度な型操作機能について学びます。これらを組み合わせることで、より型安全で表現力豊かなコードが書けるようになります。
この章で学ぶこと
- keyof演算子
- Lookup型(Indexed Access Types)
- インデックスシグネチャ
- Optional Chaining(?.)
- Nullish Coalescing(??)
- constアサーション
この章では、オブジェクトのプロパティを型レベルで操作する方法を学びます。これらはジェネリクスと組み合わせることで、表現力の高い機能になります。
keyof演算子
keyof演算子は、オブジェクト型のすべてのプロパティ名をUnion型として取得します。
interface User {
id: number;
name: string;
email: string;
age: number;
}
// keyofでプロパティ名のUnion型を取得
type UserKeys = keyof User;
// "id" | "name" | "email" | "age"
// 型安全にプロパティにアクセスする関数
function getUserProperty(user: User, key: UserKeys): string | number {
return user[key];
}
const user: User = {
id: 1,
name: "山田太郎",
email: "yamada@example.com",
age: 30
};
console.log(getUserProperty(user, "name")); // "山田太郎"
console.log(getUserProperty(user, "age")); // 30
// getUserProperty(user, "invalid"); // Error: 存在しないキー
keyofの活用例
// オブジェクトのキーを取得する関数
function getKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
const product = {
name: "ノートPC",
price: 100000,
stock: 50
};
// keys は ("name" | "price" | "stock")[] 型
const keys = getKeys(product);
console.log(keys); // ["name", "price", "stock"]
// 型安全にプロパティを列挙
keys.forEach(key => {
console.log(`${key}: ${product[key]}`);
});
Lookup型(Indexed Access Types)
Lookup型は、オブジェクト型から特定のプロパティの型を取得します。
interface User {
id: number;
name: string;
email: string;
profile: {
age: number;
address: string;
};
}
// 特定のプロパティの型を取得
type UserId = User["id"]; // number
type UserName = User["name"]; // string
type UserProfile = User["profile"]; // { age: number; address: string; }
// ネストしたプロパティの型も取得可能
type UserAge = User["profile"]["age"]; // number
// 複数のプロパティの型のUnion
type UserIdOrName = User["id" | "name"]; // number | string
// keyofと組み合わせる
type UserPropertyTypes = User[keyof User];
// number | string | { age: number; address: string; }
ジェネリックな関数での活用
// 完全に型安全なプロパティアクセス関数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
interface Product {
id: number;
name: string;
price: number;
}
const product: Product = {
id: 1,
name: "ノートPC",
price: 100000
};
// 戻り値の型が正確に推論される
const productName = getProperty(product, "name"); // string型
const productPrice = getProperty(product, "price"); // number型
// 存在しないキーを指定するとエラー
// getProperty(product, "invalid"); // Error
配列要素の型を取得
// 配列型から要素の型を取得
type ArrayElement<T extends readonly any[]> = T[number];
type StringArray = string[];
type StringElement = ArrayElement<StringArray>; // string
// タプル型にも使用可能
type Tuple = [string, number, boolean];
type TupleElements = Tuple[number]; // string | number | boolean
// 実用例:定数配列から型を生成
const COLORS = ["red", "green", "blue"] as const;
type Color = typeof COLORS[number]; // "red" | "green" | "blue"
インデックスシグネチャ
動的なプロパティ名を持つオブジェクトの型を定義できます。
基本的なインデックスシグネチャ
// 任意のプロパティ名を許可するオブジェクト
interface StringMap {
[key: string]: string;
}
const userNames: StringMap = {
user1: "山田太郎",
user2: "田中花子",
user3: "佐藤次郎"
// 任意のプロパティを追加可能
};
userNames.user4 = "鈴木三郎"; // OK
userNames.anyKey = "任意の値"; // OK
固定プロパティとの組み合わせ
interface UserData {
id: number; // 必須プロパティ
name: string; // 必須プロパティ
[key: string]: string | number; // その他の任意プロパティ
}
const user: UserData = {
id: 1,
name: "山田太郎",
age: 30, // OK: number型
email: "yamada@example.com", // OK: string型
address: "東京都" // OK: string型
};
// 注意: 固定プロパティの型はインデックスシグネチャの型と互換性が必要
numberインデックス
// 配列のようなオブジェクト
interface NumberDictionary {
[index: number]: string;
}
const fruits: NumberDictionary = {
0: "りんご",
1: "バナナ",
2: "オレンジ"
};
console.log(fruits[0]); // "りんご"
console.log(fruits[1]); // "バナナ"
Recordユーティリティ型
インデックスシグネチャの代替としてRecord型を使用できます。
// インデックスシグネチャの代替
type UserRoles = Record<string, string>;
const roles: UserRoles = {
admin: "管理者",
editor: "編集者",
viewer: "閲覧者"
};
// より型安全なアプローチ: Union型をキーに使用
type RoleName = "admin" | "editor" | "viewer";
type UserRolesStrict = Record<RoleName, string>;
const strictRoles: UserRolesStrict = {
admin: "管理者",
editor: "編集者",
viewer: "閲覧者"
// invalidRole: "無効" // Error: 許可されていないキー
};
Optional Chaining(?.)
ネストしたプロパティに安全にアクセスするための演算子です。
// ネストしたオブジェクトの型定義
interface Address {
street: string;
city: string;
}
interface UserProfile {
name: string;
address?: Address; // オプショナル
}
interface ApiResponse {
user?: UserProfile; // オプショナル
}
// Optional Chainingを使わない場合
function getCityOld(response: ApiResponse): string | undefined {
// 複数のif文が必要
if (response.user && response.user.address) {
return response.user.address.city;
}
return undefined;
}
// Optional Chainingを使う場合
function getCity(response: ApiResponse): string | undefined {
// ?.で安全にアクセス
// 途中でnull/undefinedならundefinedを返す
return response.user?.address?.city;
}
const response1: ApiResponse = {
user: {
name: "山田太郎",
address: {
street: "1-2-3",
city: "東京都"
}
}
};
const response2: ApiResponse = {
user: {
name: "田中花子"
// addressがない
}
};
const response3: ApiResponse = {};
console.log(getCity(response1)); // "東京都"
console.log(getCity(response2)); // undefined
console.log(getCity(response3)); // undefined
配列とメソッドへの適用
interface UserData {
friends?: string[];
greet?: () => void;
}
const user1: UserData = {
friends: ["Alice", "Bob"],
greet() { console.log("Hello!"); }
};
const user2: UserData = {};
// 配列へのOptional Chaining
console.log(user1.friends?.[0]); // "Alice"
console.log(user2.friends?.[0]); // undefined
// メソッドへのOptional Chaining
user1.greet?.(); // "Hello!"
user2.greet?.(); // 何も起こらない(エラーなし)
Nullish Coalescing(??)
nullまたはundefinedの場合にデフォルト値を設定する演算子です。
// Nullish Coalescing演算子
function getDisplayName(name: string | null | undefined): string {
// nameがnullまたはundefinedの場合、"ゲスト"を返す
return name ?? "ゲスト";
}
console.log(getDisplayName("山田太郎")); // "山田太郎"
console.log(getDisplayName(null)); // "ゲスト"
console.log(getDisplayName(undefined)); // "ゲスト"
||演算子との違い
// ||演算子との違いに注意
function getCount(count: number | null): number {
// ||演算子を使用
const result1 = count || 10;
// 0もfalsyなので、count=0でも10が返る
// ??演算子を使用
const result2 = count ?? 10;
// 0はnullでもundefinedでもないので、0が返る
return result2;
}
console.log(getCount(0)); // 0(||なら10)
console.log(getCount(null)); // 10
| 演算子 | falsyな値での動作 | 用途 |
|---|---|---|
|| | すべてのfalsy値で右側を返す | デフォルト値(0, ""を除外したい場合) |
?? | null/undefinedのみ右側を返す | null/undefinedのみ置換したい場合 |
実践例:オプションパラメータ
interface SearchOptions {
query: string;
limit?: number;
offset?: number;
}
function search(options: SearchOptions): void {
// ??でデフォルト値を設定
const limit = options.limit ?? 20; // デフォルト20
const offset = options.offset ?? 0; // デフォルト0
console.log(`検索: ${options.query}`);
console.log(`表示件数: ${limit}`);
console.log(`開始位置: ${offset}`);
}
search({ query: "TypeScript" });
// 検索: TypeScript
// 表示件数: 20
// 開始位置: 0
search({ query: "JavaScript", limit: 10, offset: 5 });
// 検索: JavaScript
// 表示件数: 10
// 開始位置: 5
search({ query: "Node.js", limit: 0 }); // limit=0も有効
// 検索: Node.js
// 表示件数: 0
// 開始位置: 0
constアサーション
as constを使うと、値をリテラル型かつreadonly(読み取り専用)にできます。
基本的なconstアサーション
// 通常のリテラル
let name1 = "太郎"; // string型
const name2 = "太郎"; // "太郎"型(リテラル型)
// constアサーションを使用
let name3 = "太郎" as const; // "太郎"型(リテラル型)
// 配列へのconstアサーション
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
// colors[0] = "yellow"; // Error: 読み取り専用
// colors.push("yellow"); // Error: 読み取り専用
// オブジェクトへのconstアサーション
const user = {
name: "山田太郎",
age: 30
} as const;
// user.age = 31; // Error: 読み取り専用
constアサーションの効果
| 効果 | 説明 |
|---|---|
| リテラル型 | プリミティブ値がリテラル型になる |
| readonly | すべてのプロパティが読み取り専用 |
| タプル化 | 配列が読み取り専用タプルになる |
実践例:設定オブジェクト
// 設定オブジェクトを不変に
const CONFIG = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryCount: 3,
features: ["feature1", "feature2"]
} as const;
// CONFIG型を取得
type Config = typeof CONFIG;
// {
// readonly apiUrl: "https://api.example.com";
// readonly timeout: 5000;
// readonly retryCount: 3;
// readonly features: readonly ["feature1", "feature2"];
// }
// 値の変更は不可
// CONFIG.timeout = 10000; // Error: 読み取り専用
定数からUnion型を生成
// 定数配列
const STATUS_LIST = ["pending", "active", "completed"] as const;
// 配列の要素からUnion型を生成
type Status = typeof STATUS_LIST[number];
// "pending" | "active" | "completed"
// この型を使う
function setStatus(status: Status): void {
console.log(`ステータスを${status}に変更`);
}
setStatus("pending"); // OK
setStatus("active"); // OK
// setStatus("invalid"); // Error
オブジェクトから型を生成
const ROLE_MAP = {
admin: "管理者",
editor: "編集者",
viewer: "閲覧者"
} as const;
// キーのUnion型
type RoleKey = keyof typeof ROLE_MAP;
// "admin" | "editor" | "viewer"
// 値のUnion型
type RoleValue = typeof ROLE_MAP[RoleKey];
// "管理者" | "編集者" | "閲覧者"
satisfies演算子
TypeScript 4.9で導入されたsatisfies演算子は、型チェックを行いながらリテラル型を保持できる演算子です。
基本的な使い方
// 設定の型を定義
type Config = {
theme: "light" | "dark";
fontSize: number;
features: string[];
};
// ❌ 型注釈を使う場合: リテラル型が失われる
const config1: Config = {
theme: "dark",
fontSize: 14,
features: ["search", "export"]
};
// config1.theme の型は "light" | "dark"("dark"ではない)
// ✅ satisfiesを使う場合: リテラル型を保持しながら型チェック
const config2 = {
theme: "dark",
fontSize: 14,
features: ["search", "export"]
} satisfies Config;
// config2.theme の型は "dark"(リテラル型を保持)
// config2.features の型は string[]
// さらにas constと組み合わせると完全にリテラル
const config3 = {
theme: "dark",
fontSize: 14,
features: ["search", "export"]
} as const satisfies Config;
// config3.features の型は readonly ["search", "export"]
型注釈との違い
type Color = "red" | "green" | "blue";
type ColorMap = Record<Color, string>;
// 型注釈: 値の型はstring
const colors1: ColorMap = {
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff"
};
console.log(colors1.red); // 型: string
// satisfies: 値のリテラル型を保持
const colors2 = {
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff"
} satisfies ColorMap;
console.log(colors2.red); // 型: "#ff0000"(リテラル型)
実践例:ルート定義
// ルートの型定義
type Route = {
path: string;
component: string;
requiresAuth?: boolean;
};
type Routes = Record<string, Route>;
// satisfiesで型チェックしながらリテラル型を保持
const routes = {
home: { path: "/", component: "Home" },
dashboard: { path: "/dashboard", component: "Dashboard", requiresAuth: true },
profile: { path: "/profile", component: "Profile", requiresAuth: true }
} satisfies Routes;
// キーの型を取得できる
type RouteKey = keyof typeof routes; // "home" | "dashboard" | "profile"
// 各プロパティのリテラル型も保持される
type HomePath = typeof routes.home.path; // "/"
satisfiesを使うべき場面
satisfies と型注釈の使い分けは以下の表を基準にしてください。
| 状況 | 推奨 | 理由 |
|---|---|---|
| 値をその具体型として使いたい(リテラル型が重要) | satisfies | 型チェックしつつリテラル型を保持 |
| API など契約型の定義 | 型注釈 (:) | 定義の意図を明確にする |
| 不変な設定値・マスターデータ | as const satisfies | 値を固定 + 型制約を両立 |
| 関数の引数・戻り値 | 型注釈 (:) | 関数シグネチャを厳密にする |
| 部分的な型を満たしつつ追加情報を保持 | satisfies | 型注釈だと情報が落ちるため |
Template Literal Types(TS 4.1+)
Template Literal Types は、文字列リテラル型を組み合わせて新しい文字列リテラル型を作る仕組みです。JavaScript のテンプレートリテラル構文を型レベルで再現したイメージです。
テンプレートリテラル型の構文
type Hello = `Hello, ${string}`
// Hello は "Hello, " で始まる任意の文字列型
const greeting1: Hello = 'Hello, TypeScript' // OK
const greeting2: Hello = 'Hi there' // Error
Union 型との組み合わせ
Template Literal Types は Union 型と組み合わせると、すべての組み合わせに展開されます。
type Color = 'red' | 'blue'
type Size = 'small' | 'large'
type Variant = `${Color}-${Size}`
// 'red-small' | 'red-large' | 'blue-small' | 'blue-large'
実用例: イベント名の型安全な生成
type EventName<T extends string> = `on${Capitalize<T>}`
type Events = 'click' | 'hover' | 'focus'
type HandlerProps = {
[K in Events as EventName<K>]: (e: Event) => void
}
// {
// onClick: (e: Event) => void;
// onHover: (e: Event) => void;
// onFocus: (e: Event) => void;
// }
ここで使った Capitalize<T> は 15 章で扱う文字列操作型の 1 つで、先頭を大文字化します。Template Literal Types と組み合わせることで、型安全な DSL(ドメイン固有言語)を作れるのが強みです。
詳細な Template Literal Types と infer の組み合わせは 16 章(Mapped TypesとConditional Types)で扱います。
試してみよう: 型安全なオブジェクト操作関数を作ろう ★★★
以下の要件を満たす汎用的なオブジェクト操作関数を実装してください。
要件:
getProperty関数: オブジェクトから指定したキーの値を取得updateProperty関数: オブジェクトの指定したプロパティを更新- 両関数は型安全で、存在しないキーを指定するとコンパイルエラー
interface User {
id: number;
name: string;
email: string;
age: number;
}
// 以下の関数を実装してください
// function getProperty<T, K extends keyof T>(...)
// function updateProperty<T, K extends keyof T>(...)
ヒント
K extends keyof Tで、KをTのプロパティ名に制約T[K]で、対応するプロパティの型を取得- updatePropertyはスプレッド構文で新しいオブジェクトを返す
回答と解説
interface User {
id: number;
name: string;
email: string;
age: number;
}
// プロパティを取得する関数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// プロパティを更新する関数(イミュータブルに)
function updateProperty<T, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): T {
return {
...obj,
[key]: value
};
}
const user: User = {
id: 1,
name: "山田太郎",
email: "yamada@example.com",
age: 30
};
// getPropertyの使用
const userName = getProperty(user, "name"); // string型
const userAge = getProperty(user, "age"); // number型
console.log(userName); // "山田太郎"
console.log(userAge); // 30
// 存在しないキーはエラー
// getProperty(user, "invalid"); // Error
// updatePropertyの使用
const updated = updateProperty(user, "name", "田中花子");
console.log(updated);
// { id: 1, name: "田中花子", email: "yamada@example.com", age: 30 }
// 型が一致しないとエラー
// updateProperty(user, "name", 123); // Error: string型が必要
// 存在しないキーはエラー
// updateProperty(user, "invalid", "x"); // Error
解説:
K extends keyof Tにより、KはTの有効なプロパティ名に制約されますT[K](Lookup型)により、対応するプロパティの型を取得できます- これにより、存在しないキーの指定や、型が一致しない値の設定がコンパイルエラーになります
- updatePropertyはスプレッド構文で元のオブジェクトを変更せず、新しいオブジェクトを返します(イミュータブル)
まとめ
この章で学んだこと:
- keyof演算子: オブジェクト型のプロパティ名をUnion型で取得
- Lookup型(
T[K]): プロパティの型を取得 - インデックスシグネチャ: 動的なプロパティ名を持つオブジェクトの型定義
- Optional Chaining(
?.): null/undefinedを安全にチェーンアクセス - Nullish Coalescing(
??): null/undefinedのみデフォルト値を適用 - constアサーション: リテラル型かつreadonly化
これらの機能を組み合わせることで、型安全で保守性の高いコードが書けます。
次の章では、ジェネリクスの基礎について学びます。
初学者がつまずきやすいポイント
よくある間違い
❌ keyofでプリミティブ型のキーを取得しようとする
// ❌ プリミティブ型にkeyofを使用
type StringKeys = keyof string;
// 結果: number や組み込みメソッド名を含むUnion型(number | "toString" | "charAt" | ...)
// 意図した型ではない可能性が高い
const key: StringKeys = "charAt"; // OK だが意図通り?
原因: keyofはオブジェクト型のキーを取得するもので、プリミティブに使うと予期しない結果になります。
解決策: keyofはオブジェクト型(interface、type alias)に対して使用しましょう。
❌ インデックスシグネチャと固定プロパティの型が不整合
// ❌ 固定プロパティの型がインデックスシグネチャと合わない
interface BadConfig {
name: string;
count: number; // Error: インデックスシグネチャと互換性がない
[key: string]: string;
}
// ✅ 固定プロパティの型をインデックスシグネチャに含める
interface GoodConfig {
name: string;
count: number;
[key: string]: string | number; // すべての値の型を含む
}
原因: インデックスシグネチャは「すべてのプロパティの型」を定義するため、固定プロパティもこの型に含まれる必要があります。 解決策: インデックスシグネチャの値の型を、固定プロパティの型を含むUnion型にしましょう。
❌ Optional Chainingの結果型を考慮しない
interface User {
profile?: {
age: number;
};
}
// ❌ 結果がundefinedの可能性を考慮しない
function getAge(user: User): number {
return user.profile?.age; // Error: number | undefinedをnumberに代入できない
}
// ✅ undefinedの場合を考慮
function getAgeSafe(user: User): number {
return user.profile?.age ?? 0; // デフォルト値を設定
}
原因: ?.の結果は常に| undefinedが追加されます。
解決策: ??でデフォルト値を設定するか、戻り値型に| undefinedを含めましょう。
❌ as constを付け忘れて型が広がる
// ❌ as constなし
const COLORS = ["red", "green", "blue"];
type Color = typeof COLORS[number];
// Color は string型(リテラル型ではない)
// ✅ as constを付ける
const COLORS_CONST = ["red", "green", "blue"] as const;
type ColorConst = typeof COLORS_CONST[number];
// ColorConst は "red" | "green" | "blue"
原因: 通常の配列は要素の型が広がり(string[]など)、リテラル型が失われます。
解決策: リテラル型を保持したい場合はas constを付けましょう。
❌ ||と??の違いを理解していない
// ❌ 0や""もfalseとして扱われてしまう
function getCount(value: number | null): number {
return value || 10; // value=0でも10が返る
}
console.log(getCount(0)); // 10(意図しない動作)
console.log(getCount(null)); // 10
// ✅ null/undefinedのみを置換
function getCountSafe(value: number | null): number {
return value ?? 10; // value=0なら0が返る
}
console.log(getCountSafe(0)); // 0
console.log(getCountSafe(null)); // 10
原因: ||はfalsy値(0, "", falseなど)すべてでデフォルト値を返します。
解決策: null/undefinedのみを置換したい場合は??を使いましょう。
次に読む
ジェネリクス へ進みます。汎用的に再利用できる型パラメータの設計とユーティリティ型を学びます。