TypeScriptには、より柔軟で強力な型の表現を可能にする高度な型機能が用意されています。このセクションでは、インデックスシグネチャ、keyof
と型の操作、条件型(Conditional Types)、そしてマップ型(Mapped Types)について説明します。
インデックスシグネチャ
インデックスシグネチャを使うことで、オブジェクトが複数のプロパティを持ち、それらのプロパティが特定の型を持つことを定義できます。主に動的にプロパティを扱う場合に使用されます。
typescript
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Alice", "Bob", "Charlie"];
console.log(myArray[1]); // Bob
上記の例では、StringArray
インターフェースは任意の数値インデックスを持つことができ、それぞれの値がstring
型であることを定義しています。これにより、配列のような構造を型で表現することができます。
typescript
interface Dictionary {
[key: string]: number;
}
let scores: Dictionary = {
alice: 90,
bob: 85,
charlie: 95,
};
console.log(scores["alice"]); // 90
この例では、Dictionary
インターフェースを使って、任意の文字列キーを持つオブジェクトを定義し、その値がnumber
型であることを示しています。
keyof
と型の操作
keyof
演算子を使うと、オブジェクト型のすべてのキーを文字列リテラル型として取得できます。これにより、動的にオブジェクトのキーを操作することが可能になります。
typescript
interface Person {
name: string;
age: number;
isAdmin: boolean;
}
type PersonKeys = keyof Person; // "name" | "age" | "isAdmin"
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let person: Person = { name: "Alice", age: 30, isAdmin: true };
let name = getProperty(person, "name"); // OK, name は string 型
let age = getProperty(person, "age"); // OK, age は number 型
// let invalid = getProperty(person, "address"); // エラー: 'address' は 'Person' のキーではありません
keyof
を使うことで、オブジェクトのキーを動的に扱う関数を定義し、型安全にプロパティを参照することができます。
Conditional Types(条件型)
条件型は、extends
キーワードを使って型を条件によって変化させることができる強力な機能です。条件に応じて異なる型を返すことで、柔軟な型定義を可能にします。
typescript
type IsString<T> = T extends string ? "string" : "not string";
type Test1 = IsString<string>; // "string"
type Test2 = IsString<number>; // "not string"
上記の例では、IsString
という型を定義し、T
がstring
型であれば"string"
というリテラル型を返し、それ以外の場合は"not string"
を返します。これにより、型を条件によって動的に変更することができます。
また、条件型を使うことで型のフィルタリングも可能です。
typescript
type Exclude<T, U> = T extends U ? never : T;
type NonString = Exclude<"a" | "b" | 1 | 2, string>; // 1 | 2
この例では、Exclude
を使って、T
のうちU
に一致する型を除外しています。これにより、指定した型を取り除いた新しい型を生成することができます。
Mapped Types(マップ型)
マップ型を使うことで、既存の型から新しい型を作成することができます。マップ型は、既存のプロパティの型を変更したり、すべてのプロパティに対して同じ処理を適用する場合に使用します。
typescript
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
let person: ReadonlyPerson = {
name: "Alice",
age: 30,
};
// person.age = 31; // エラー: 'age' は読み取り専用です
上記の例では、ReadonlyPerson
という型を作成しています。この型はPerson
型のすべてのプロパティにreadonly
修飾子を追加しています。これにより、オブジェクトのプロパティを読み取り専用にすることができます。
さらに、プロパティの型を変更することも可能です。
typescript
type Optional<T> = {
[K in keyof T]?: T[K];
};
type OptionalPerson = Optional<Person>;
let optionalPerson: OptionalPerson = {
name: "Bob",
};
この例では、Optional
というマップ型を定義し、すべてのプロパティをオプショナルにしています。これにより、Person
型のプロパティがすべて省略可能となったOptionalPerson
型を作成しています。
まとめ
高度な型機能を使うことで、TypeScriptの型システムをより強力に活用することができます。インデックスシグネチャを使うことで動的なプロパティを持つオブジェクトを定義し、keyof
や条件型を使って型を柔軟に操作できます。さらに、マップ型を使うことで既存の型を再利用しながら新しい型を簡単に作成することができます。これらの高度な型機能を駆使することで、より安全で表現力の高いコードを書くことができるようになるでしょう。