型演算子

6 min読了の目安(約5800字TECH技術記事

はじめに

今回の記事では、インターフェースや型エイリアスなどで表されたオブジェクト型の中で利用される型の演算子を扱います。


keyof演算子

keyof演算子は、オブジェクトの全てのプロパティを文字列リテラル型のユニオン型で取得できます。

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

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

多階層で構成されたオブジェクトについては以下のように型を取得できます。

interface Obj {
  person: { name: string; age: number };
  book: { name: string; price: number };
}

type PropUser = keyof Obj; //型は "user1" | "user2"
type PropUser1 = keyof Obj["person"]; //型は"name" | "age"
type PropUser2 = keyof Obj["book"]; //型は"name" | "price"

ドット記法は変数を使ってプロパティにアクセスできないため、ブラケット記法を用いています。


keyof演算子のユースケース

①ジェネリクスとの共存

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

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

const  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型はオブジェクトであるとTypeScriptが推測しているためだと思われます。

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

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

const 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(valueOfUser(iPhone, "user1")); //{name: "Max", age: 22}
console.log(valueOfUser(iPhone, "user2")); //{name: "Green", age: 21}

②typeof演算子との共存

keyof演算子については以前型ガードの記事で扱いました。是非参考にして下さい!
keyofとtypeofと合わせると、既存のオブジェクトからプロパティ名をリテラル型として抽出できるようになります。

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

prop4は変数objに存在しないため、propOfObj4にエラーが生じています。

因みに、既存のオブジェクトからkeyofのみでプロパティ名をリテラル型として抽出することはできません。vscodeであれば親切にエラーの内容を表示してくれます。


ルックアップ型

ルックアップ型とは、T[K]という形の構文で表しオブジェクト型に対してプロパティ名で値の型レベルにアクセスするようなものです。

interface Person {
  name: string;
  age: number;
}
type Typeofname = Person["name"]; //型はstring;
type Typeofage = Person["age"]; //型はnumber;

因みに、プロパティにオプションである?をつけると,ブジェクトのプロパティに対応するvalueの型とundifinedとのユニオン型で型を取得できます。

interface Person {
  name: string;
  age?: number;
}
type Typeofname = Person["name"]; //string;
type Typeofage = Person["age"]; //number|undifined;

多階層で構成されたオブジェクトについては以下のように型レベルでアクセスできます。

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

type A = User["user1"];
//型は{name: string;age: number;}

type B = User["user1"]["name"];
//型はstring

type C = User["user1"]["age"];
//型はnumber

ルックアップ型のユースケース

①配列との共存

配列の場合、キーとして[number]を指定することで、要素を取り出せます。

type Hoge = Array<string>;
type A = Hoge[number]; //型はstring

type Foo = string[];
type B = Foo[number]; //型はstring

type Bar = (string | boolean)[];
type C = Bar[number]; // 型はstring | boolean

配列のサブタイプであるタプル型については、キーとしてインデックスを指定して要素を取り出せます。

type Foo = [string, number];
type A = Foo[0]; //型はstring
type B = Foo[1]; //型はnumber
type C = Foo[number]; //型はstring | boolean

②keyof演算子との共存

先程のkeyof演算子と利用することも可能です。

interface Person {
  name: string;
  age: number;
}
type A = Person[keyof Person];
//型はstring | number

このようにオブジェクトの各プロパティに対するvalueの型をユニオン型で取得できます。

③ジェネリクスとの共存

ジェネリクスを用いて、型を関数のように扱うことが可能です。

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

type ValueOfType<T, U extends keyof T> = T[U];
type Typeofname = ValueOfType<Person, "name">;
//型はstring

またオブジェクトから一部の値を取り出す関数を作り出すことができます。

type User = {
  id: number;
  name: string;
  note?: string;
};
type APIResponse = {
  user: User;
  isPremiumUser: boolean;
};

const extractFromAPIResponse = <T, U extends keyof T>(
  obj: T,
  key: U
): T[keyof T] => {
  return obj[key];
};

const sampleData: APIResponse = {
  user: {
    id: 1,
    name: "Alice",
  },
  isPremiumUser: true,
};
console.log(extractFromAPIResponse(sampleData, "user"));
//{id: 1, name: "Alice"}

console.log(extractFromAPIResponse(sampleData, "isPremiumUser"));
//true

マップ型(Mapped Type)

型の値を取得する目的でin演算子を利用するケースにマップ型が使われます。つまり、インターフェースや型エイリアスの中で使われているin演算子はマップ型です。

type Fig = "one" | "two" | "three";
type FigMap = { [k in Fig]: number };
const figMap: FigMap = {
  one: 1,
  two: 2,
  three: 3,
};

型エイリアス内、オブジェクトの型を定義しています。そのオブジェクトのプロパティにマップ型を利用しています。
ここではfigMapの変数にFigMapを型指定し、Figという文字列リテラル型の中からone,two,threeをオブジェクトのプロパティ名として使うよう強制させます。

またここではone,two,three全てをプロパティとして使わないとエラーが出ますが、その一部(oneとtwoだけなど)をプロパティとして使いたい場合はオプションである?を使います。

type Fig = "one" | "two" | "three";
type FigMap = { [k in Fig]?: number };
const figMap: FigMap = {
  one: 1,
  two: 2,
};

マップ型のユースケース

マップ型をkyeof演算子、ルックアップ型と組み合わせると、オブジェクト型に対してどの値の型がどのプロパティ名に対応するかを制約できます。

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

type PieceOfPerson = {
  [K in keyof Person]?: Person[K];
};

const obj: PieceOfPerson = {
  name: "max", //ageは省略可
};