【TypeScript チュートリアル】 `keyof` 演算子を使ってオブジェクトを操作する基本から応用まで
TypeScript の keyof
演算子と型推論
例えば、以下のオブジェクトを定義します。
interface Person {
name: string;
age: number;
}
この場合、keyof Person
は 'name' | 'age'
という型になります。これは、Person
オブジェクトのプロパティ名は 'name'
または 'age'
のいずれかであることを意味します。
しかし、keyof
演算子は、Person
オブジェクトのプロパティ名に文字列以外の型が使用される可能性を考慮しません。例えば、以下のコードを実行します。
const person: Person = { name: 'John Doe', age: 30 };
const key: keyof Person = 0; // エラー: 'keyof Person' は 'string' | 'number' ですが、'0' は 'string' | 'number' のいずれにも一致しません。
const value: Person[key] = person[key]; // エラー: 'Person[key]' は 'string' | 'number' ですが、'string' | 'number' は 'string' と互換性がありません。
このコードは、key
変数に 0
を代入し、person[key]
によってその値を取得しようとします。しかし、keyof Person
は 'name'
または 'age'
という型のみを返します。そのため、0
は keyof Person
に一致せず、エラーが発生します。
型推論を使用した keyof
演算子の制限の克服
TypeScript 4.0 以降では、型推論を使用して keyof
演算子の制限を克服することができます。これは、infer
キーワードを使用して、keyof
演算子によって返される型を推論することで実現できます。
例えば、以下のコードを実行します。
type KeyOfPerson<T extends Person> = keyof T; // 'name' | 'age'
const key: KeyOfPerson<Person> = 'name'; // 型推論により 'name' となる
const value: Person[key] = person[key]; // 型推論により 'string' となる
このコードでは、KeyOfPerson
型パラメータを使用して、keyof
演算子によって返される型を推論します。KeyOfPerson<Person>
は 'name' | 'age'
という型になります。そのため、key
変数は 'name'
型になり、person[key]
は 'string'
型になります。
TypeScript の keyof
演算子は、オブジェクトのすべてのプロパティ名の型を表現するユニオン型を返します。しかし、keyof
演算子は、常に文字列または数値の型を返します。TypeScript 4.0 以降では、型推論を使用して keyof
演算子の制限を克服することができます。
keyof
演算子の制限を克服するには、Mapped Types
やConditional Types
などの他の機能を使用することもできます。- 型推論は、コンパイラの推論能力に依存するため、常に正確であるとは限りません。
infer
キーワードは、TypeScript 4.0 以降で使用できます。
例 1: オブジェクトのプロパティ名と値を配列に格納
この例では、keyof
演算子を使用してオブジェクトのプロパティ名を配列に格納し、その配列をループして各プロパティの値を出力します。
interface Person {
name: string;
age: number;
occupation: string;
}
const person: Person = {
name: 'John Doe',
age: 30,
occupation: 'Software Engineer'
};
const keys: (keyof Person)[] = Object.keys(person); // 'name' | 'age' | 'occupation' の型
for (const key of keys) {
console.log(`${key}: ${person[key]}`); // name: John Doe, age: 30, occupation: Software Engineer
}
例 2: 特定の型のプロパティのみを処理
この例では、keyof
演算子とジェネリック型を使用して、特定の型のプロパティのみを処理します。
interface Product {
name: string;
price: number;
stock: number;
}
const products: Product[] = [
{ name: 'Laptop', price: 1000, stock: 10 },
{ name: 'Phone', price: 500, stock: 5 },
{ name: 'Tablet', price: 300, stock: 20 },
];
function printProductDetails<T extends Product>(product: T, keys: (keyof T)[]): void {
for (const key of keys) {
console.log(`${key}: ${product[key]}`);
}
}
const productNames: (keyof Product)[] = ['name', 'price']; // 'name' | 'price' の型
const product1 = products[0];
printProductDetails(product1, productNames); // name: Laptop, price: 1000
例 3: インデックス型を使用して動的なキーにアクセス
この例では、keyof
演算子とインデックス型を使用して、動的なキーにアクセスします。
interface User {
id: number;
[key: string]: any; // 任意のプロパティを許可
}
const users: User[] = [
{ id: 1, name: 'John Doe', age: 30 },
{ id: 2, name: 'Jane Doe', email: '[email protected]' },
];
function getUserProperty<T extends User, K extends keyof T>(userId: number, key: K): T[K] | undefined {
const user = users.find(user => user.id === userId);
if (user) {
return user[key];
} else {
return undefined;
}
}
const userName = getUserProperty(1, 'name'); // userName は 'John Doe' となる
const userEmail = getUserProperty(2, 'email'); // userEmail は '[email protected]' となる
ジェネリック型を使用して、オブジェクトの型をパラメータとして渡すことができます。これにより、コンパイラはオブジェクトのプロパティ名の型をより正確に推論することができます。
interface User<T> {
id: number;
[key: string]: T; // 任意のプロパティを許可
}
function getUserProperty<T extends User<T>, K extends keyof T>(user: User<T>, key: K): T[K] | undefined {
return user[key];
}
const userName = getUserProperty({ id: 1, name: 'John Doe' }, 'name'); // userName は 'John Doe' となる
const userEmail = getUserProperty({ id: 2, email: '[email protected]' }, 'email'); // userEmail は '[email protected]' となる
typeof 演算子を使用する
typeof
演算子を使用して、オブジェクトの型を取得し、その型のプロパティ名の型を表現するユニオン型を作成することができます。
interface User {
id: number;
name: string;
email: string;
}
const user: User = { id: 1, name: 'John Doe', email: '[email protected]' };
const keys: (keyof User)[] = Object.keys(user) as (keyof typeof user)[]; // 'id' | 'name' | 'email' の型
for (const key of keys) {
console.log(`${key}: ${user[key]}`); // name: John Doe, age: 30, occupation: Software Engineer
}
Mapped Types を使用する
Mapped Types
を使用して、オブジェクトのプロパティ名の型を個別に指定することができます。
type UserKeys = {
id: number;
name: string;
email: string;
};
type UserProperty<K extends keyof UserKeys> = UserKeys[K];
function getUserProperty<K extends keyof UserKeys>(user: User, key: K): UserProperty<K> | undefined {
return user[key];
}
const userName = getUserProperty(user, 'name'); // userName は 'John Doe' となる
const userEmail = getUserProperty(user, 'email'); // userEmail は '[email protected]' となる
Conditional Types を使用する
Conditional Types
を使用して、オブジェクトのプロパティ名の型に応じて異なる型を返すことができます。
type UserProperty<K extends keyof User> =
K extends 'id' ? number :
K extends 'name' | 'email' ? string :
never;
function getUserProperty<K extends keyof User>(user: User, key: K): UserProperty<K> | undefined {
return user[key];
}
const userName = getUserProperty(user, 'name'); // userName は 'John Doe' となる
const userEmail = getUserProperty(user, 'email'); // userEmail は '[email protected]' となる
これらの方法はそれぞれ異なる利点と欠点があります。状況に応じて適切な方法を選択する必要があります。
typescript typescript-typings