Typescript(keyof演算子)

はじめに

今回の記事は、オブジェクトの全てのプロパティを、文字列リテラル型のユニオン型(合併)で取得できるkeyof演算子についてです。
関連する記事として、以前書いた記事を参考にした上で読み進めてもらうと理解が深まると思います。
Typescript(リテラル型) - 駆け足エンジニアの記録
Typescript(合併型と交差型) - 駆け足エンジニアの記録

keyofの基本

keyofについては記述も簡単でその結果も直感的でわかりやすいです。

interface Person {
  name: string;
  age: number;
}

type PropPerson = keyof Person;
//PropPersonの型は"name" | "age"

let a: PropPerson = "name";
let b: PropPerson = "age";

多階層で構成されたオブジェクトについては以下のような結果となります。

interface User {
  user1: { name: string; age: number };
  user2: { name: string; age: number };
}

type PropUser = keyof User; //型は "user1" | "user2"
type PropUser1 = keyof User["user1"]; //型は"name" | "age"
type PropUser2 = keyof User["user2"]; //型は"name" | "age"

このようにブラケット記法によりオブジェクト内のプロパティにアクセスできます。(ドット記法ではプロパティを取得できません)

keyofの応用

ジェネリクス型との共存

ジェネリクスとkeyofを組み合わせることで、オブジェクト内の指定されたプロパティの値を取得することができます。

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}

function valueOfPerson<O extends object, K extends keyof O>(o: O, k: K): O[K] {
  return o[k];
}
const max: Person = {
  name: "Max",
  age: 22,
  gender: "male",
};
console.log(valueOfPerson(max, "name")); //Max
console.log(valueOfPerson(max, "age")); //22
console.log(valueOfPerson(max, "gender")); //male

ジェネリクス型パラメーターを宣言している所でkeyofが登場しています。ここではK型とO型の関係を定義しているため省略することはできません。特に2つ目のK extends keyof OはOがオブジェクト、Kがそのオブジェクトのプロパティであることを記載しているため省略するとエラーとなります。
因みに、山括弧内1つ目のO extends objectのextends objectは省略してもエラーを出しませんでした。おそらく2つ目のkeyofのおかげでO型はオブジェクトであると推測しているためだと思われます。


また多階層のオブジェクト対しては入れ子になったオブジェクトを生成します。

interface User {
  user1: { name: string; age: number };
  user2: { name: string; age: number };
}

function valueOfUser<O extends object, K extends keyof O>(o: O, k: K): O[K] {
  return o[k];
}
const iPhone: User = {
  user1: { name: "Max", age: 22 },
  user2: { name: "Green", age: 21 },
};
console.log(valueOfPerson(iPhone, "user1")); //{name: "Max", age: 22}
console.log(valueOfPerson(iPhone, "user2")); //{name: "Green", age: 21}

②typeofとの共存
keyof単体ではオブジェクトの型定義(インターフェースや型エイリアス)からのみプロパティを文字列リテラル型のユニオン型で取得可能でしたが、typeof と合わせると、既存のオブジェクトからプロパティの型を抽出できるようになります。

const obj = {
  prop1: "value1",
  prop2: "value2",
  prop3: "value3",
};
type PropOfObj = keyof typeof obj;
const propOfObj1: PropOfObj = "prop1"; //OK
const propOfObj2: PropOfObj = "prop2"; //OK
const propOfObj3: PropOfObj = "prop3"; //OK
const propOfObj4: PropOfObj = "prop4"; //エラー