はじめに
オブジェクトのkeyを動的に設定するための書き方として、インデックスシグネチャというものがあります。
そのインデックスシグネチャを定義する際、以下のような書き方があることを知りました。
type User<T> = {
[k in keyof T]: string;
};
この記事ではin
とtypeof
のそれぞれについて触れ、上記の型が表す意味を解説します。
inの役割
in
はインデックスシグネチャに使用される構文で、Mapped Typesと呼ばれる型を構築します。
インデックスシグネチャ
そもそもインデックスシグネチャとはオブジェクトのkeyをあらかじめ宣言することなく、動的にあとから追加できるようにするものです。
type User = {
[k: string]: string;
};
const useData: User = {
name: "Kevin",
country: "America",
};
User
型はstring
型のkeyとstring
型の値を持つオブジェクトとして定義しています。
そしてuserData
はUser
型となっており、型の定義に合致するプロパティであれば自由に追加ができます。
しかし、あまりにも自由に追加されるのも困ることがあります。
ある程度制限をかけたいときに使えるのがin
です。
基本のin
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiProcess = {
[k in HttpMethod]: () => void;
};
const apiProcess: ApiProcess = {
GET: () => {
console.log("get");
},
POST: () => {
console.log("post");
},
PUT: () => {
console.log("put");
},
DELETE: () => {
console.log("delete");
},
};
まず、HttpMethod
というユニオン型を用意しました。
そして、インデックスシグネチャの中で、in
のあとにユニオン型を指定しています。
これにより、ApiProcess
という型はHttpMethod
に定義された4つの値をキーに持つオブジェクトであるというふうに定義できました。
in
を使わない場合、ApiProcess
は以下のように書く必要があります。
type ApiProcess = {
GET: () => void;
POST: () => void;
PUT: () => void;
DELETE: () => void;
};
同じようなことを繰り返し書いているので、冗長に見えます。
in
を使うことで、設定したいkeyを制限しつつ、同じことを繰り返し書かずともオブジェクトの型を定義できます。
その他のin
上記で確認したのはMapped TypesというTypeScriptにおけるin
でしたが、他にもin
を使った構文は存在します。
ややこしいので、念の為整理しておきます。
for...in
ループ処理で使用する構文です。
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(`${key}: ${obj[key]}`);
}
判定のin
指定した値がオブジェクトのプロパティに存在するかどうかを判定します。
const animal = {
dog: "わんわん",
cat: "にゃーにゃー",
};
if ("dog" in animal) {
console.log(animal.dog);
}
これらはもともとJavaScriptにも用意されている構文です。
今回確認したMapped Typesとは関係がないので注意してください。
keyofの役割
過去の記事で一度keyof
については触れていますが、改めて確認します。
keyof
演算子はオブジェクトのプロパティだけをまとめて取得して、リテラルのユニオン型として設定することができます。
type Person = {
name: string;
age: number;
address: string;
}
type PersonKeys = keyof Person;
// type PersonsKeys = 'name' | 'age' | 'address'と定義するのと同じ
これにより、オブジェクトのプロパティを動的に取得することができます。
type Person = {
name: string;
age: number;
address: string;
};
const user: Person = {
name: "Kevin",
age: 28,
address: "address",
};
const printPerson = (param: keyof Person) => {
console.log(user[param]);
};
in keyof Tの意味
ここまでくれば、何を表しているかがわかるかと思います。
in
はユニオン型をもとにオブジェクトのkeyを決定します。
そして、keyof
はオブジェクトのプロパティからユニオン型を生成します。
つまり、あるオブジェクトのプロパティと同じkeyを持つことを指定できる書き方がin keyof T
になります。
in keyof Tの使用例
うまく使うことで、以下のような動的なバリデーションの処理を作成することができます。
interface FormData {
username: string;
age: number;
email: string;
password: string;
}
// バリデーションルール用の型を定義
// Tのプロパティの型をアロー関数の引数の型として設定
type ValidationRules<T> = {
[k in keyof T]: (value: T[k]) => string | null;
};
// バリデーションルールのオブジェクトを作成
// バリデーションルール用の型のTにはFormDataが設定される
const validationRules: ValidationRules<FormData> = {
username: (value) =>
value.length < 3 ? "ユーザー名は3文字以上である必要があります" : null,
age: (value) => (value > 100 ? "年齢は100未満で入力してください" : null),
email: (value) =>
/@/.test(value) ? null : "有効なメールアドレスを入力してください",
password: (value) =>
value.length < 8 ? "パスワードは8文字以上である必要があります" : null,
};
// フォームにバリデーションを実施する処理
// 戻り値の型や、エラー情報の型にもin keyofを使用
function validateForm<T>(
data: T,
rules: ValidationRules<T>
): { [k in keyof T]?: string } {
const errors: Partial<{ [k in keyof T]: string }> = {};
// ここでのinはループ。Mapped Typesではない
for (const key in data) {
if (key in rules) {
const error = rules[key](data[key]);
if (error) {
errors[key] = error;
}
}
}
return errors;
}
まとめ
in keyof
を使うことで、型安全を保ちながら柔軟な処理を書くことができました。
一見よくわからない処理でも、一つずつ紐解いていくことで何をしているのかを明らかにすることができます。
もっと型に関連する知識を沢山身につけていきたいと思いました。