TypeScript: `keyof` と `typeof` で柔軟な型定義を行う
TypeScript でオブジェクトのキーを推論し、値の型を定義する方法
しかし、オブジェクトのキーは推論できるものの、値の型は推論できない場合があります。そのような場合、オブジェクトの値の型を明示的に定義する必要があります。
この問題は、いくつかの方法で解決できます。
型注釈を使用する
最も単純な方法は、型注釈を使用してオブジェクトの値の型を明示的に定義することです。
let user: {
name: string;
age: number;
};
user = {
name: "John Doe",
age: 30
};
この例では、user
変数にオブジェクトリテラルを代入しています。オブジェクトリテラルには name
と age
という 2 つのプロパティがあり、それぞれ string
型と number
型であることが型注釈によって明示的に定義されています。
インターフェースを使用する
より複雑なオブジェクトリテラルを扱う場合は、インターフェースを使用すると便利です。インターフェースは、オブジェクトの構造を定義するための型エイリアスです。
interface User {
name: string;
age: number;
}
let user: User;
user = {
name: "John Doe",
age: 30
};
この例では、User
という名前のインターフェースを定義しています。このインターフェースには name
と age
という 2 つのプロパティがあり、それぞれ string
型と number
型であることが定義されています。その後、user
変数に User
型を指定しています。
ジェネリックを使用する
さらに汎用的な方法として、ジェネリックを使用することができます。ジェネリックは、型パラメーターを使用して、さまざまな型の値を扱うことができる型を定義する方法です。
function mapObject<T>(obj: { [key: string]: T }, fn: (value: T) => T): { [key: string]: T } {
const result: { [key: string]: T } = {};
for (const key in obj) {
result[key] = fn(obj[key]);
}
return result;
}
const users: { [key: string]: User } = {
john: { name: "John Doe", age: 30 },
jane: { name: "Jane Doe", age: 25 }
};
const updatedUsers = mapObject(users, (user) => {
user.age += 1;
return user;
});
この例では、mapObject
という名前のジェネリック関数を作成しています。この関数は、オブジェクトと、オブジェクトの値を処理する関数を受け取り、処理された値を持つ新しいオブジェクトを返します。users
変数は、User
型の値を持つキーと値のペアのオブジェクトです。updatedUsers
変数は、mapObject
関数を使用して users
オブジェクトを処理した結果です。mapObject
関数は、users
オブジェクトの各値に対して user.age += 1
を実行し、処理された値を持つ新しいオブジェクトを返します。
これらの方法は、TypeScript でオブジェクトのキーを推論し、値の型を定義する方法のほんの一例です。状況に応じて適切な方法を選択してください。
let user: {
name: string;
age: number;
};
user = {
name: "John Doe",
age: 30
};
interface User {
name: string;
age: number;
}
let user: User;
user = {
name: "John Doe",
age: 30
};
function mapObject<T>(obj: { [key: string]: T }, fn: (value: T) => T): { [key: string]: T } {
const result: { [key: string]: T } = {};
for (const key in obj) {
result[key] = fn(obj[key]);
}
return result;
}
const users: { [key: string]: User } = {
john: { name: "John Doe", age: 30 },
jane: { name: "Jane Doe", age: 25 }
};
const updatedUsers = mapObject(users, (user) => {
user.age += 1;
return user;
});
keyof と typeof を使用する
keyof
演算子は、オブジェクトのすべてのプロパティ名の型をユニオン型として取得するために使用できます。 typeof
演算子は、値の型を取得するために使用できます。これらの演算子を組み合わせて、オブジェクトの値の型を定義することができます。
type UserKeys = keyof User;
type UserValues = typeof User[UserKeys];
let user: { [key in UserKeys]: UserValues };
user = {
name: "John Doe",
age: 30
};
この例では、UserKeys
型は User
インターフェースのすべてのプロパティ名の型をユニオン型として定義します。UserValues
型は、UserKeys
型の各プロパティの値の型を定義します。user
変数は、UserKeys
型のキーと UserValues
型の値を持つオブジェクトであることが型注釈によって定義されています。
Mapped Types を使用する
Mapped Types
は、既存の型に基づいて新しい型を定義する方法です。 keyof
と typeof
演算子と組み合わせて使用することで、オブジェクトの値の型を定義することができます。
type UserWithReadonlyAge = {
[key in keyof User]: Readonly<User[key]>;
};
let user: UserWithReadonlyAge;
user = {
name: "John Doe",
age: 30
};
この例では、UserWithReadonlyAge
型は User
インターフェースと同じプロパティ名を持つ新しい型を定義しますが、すべてのプロパティの値が readonly になります。つまり、user
変数内の age
プロパティを変更することはできません。
カスタムユーティリティ型を使用する
独自のユーティリティ型を作成して、オブジェクトのキーを推論し、値の型を定義することもできます。
type AsConst<T> = { [P in keyof T]: T[P] as const };
type UserAsConst = AsConst<User>;
let user: UserAsConst;
user = {
name: "John Doe",
age: 30
};
この例では、AsConst
型は、引数として渡された型のすべてのプロパティの値を readonly にする新しい型を定義します。UserAsConst
型は、User
インターフェースと同じプロパティ名を持つ新しい型を定義しますが、すべてのプロパティの値が readonly になります。つまり、user
変数内の name
と age
プロパティを変更することはできません。
typescript