前提
なぜこの記事を書こうと思ったのか
現場での実装経験を通じて、TypeScriptを使用することで直面する困りごとが徐々に減ってきました。
そこで、一歩進んだスキルを身につけるために、TypeScriptで重要とされる「ユニオン型」に焦点を当て、基本的な使い方や今まで理解が浅かったポイント、つまずいた点をまとめた記事を書いてみようと思います。
実際keyof typeofでのユニオン型は使わなくても動くものは作れてしまう
TypeScriptを使う上で「ユニオン型が重要」とよく言われますが、個人的な開発や現場でのコードレビューの経験上、keyof typeofを使ったユニオン型についてはこれまであまり意識的に使ってきませんでした。
もちろん、パイプ(|)を使った基本的なユニオン型は普通に活用していました。例えば、string | numberのように複数の型を許容する記述は、日常的に使ってきたと思います。しかし、keyof typeofを使ったユニオン型となると、「そもそも何をするための仕組みなのか?」という点で理解が浅く、実際に利用する場面もほとんどありませんでした。
手動テストや簡単な型指定だけで十分に動作確認を行えてしまい、keyof typeofを活用する必要性を感じる機会が少なかったのだと思います。ただし、anyは使わずに型安全を意識したコードを書くようにはしていました。
でもやっぱり抑えておきたいと思った理由
しかし、チーム開発でTypeScriptを使用する場面では、保守性の高いコードを書く人たちがユニオン型を積極的に使っていることに気が付きました。
特にコードレビューを担当する立場になってからは、ユニオン型が絡むコードを理解することが重要であると感じるようになりました。
「そもそもこのコードは何をしているんだろう?」と迷うことを減らすためにも、ユニオン型の基礎をしっかり押さえておく必要があると考えています。
そもそもユニオン型とは?
TypeScriptのユニオン型とは、複数の型のいずれかを取ることができる型を表現する方法で、|(パイプ記号)を使って記述します。言い換えると、「or的な関係」を定義するものです。
以下は、stringまたはnumberのどちらかを許容するユニオン型の例です。
// stringまたはnumberを使用する型を定義する
type StrigOrNumber: string | number;
const str: StringOrNumber = "message"; //OK
const num: StringOrNumber = 1000; //OK
// 配列は型定義に該当していないためエラー
const arr: StringOrNumber = [1,2,3,4,5];
// エラー: 型 'number[]' を 'StringOrNumber' に割り当てることはできません。
keyof typeofを使ったコードを書いてみる。
import { Button } from "@mui/material";
import { FC } from "react";
// Recordとユニオンで使う型定義
type SampleObjectRecord = Record<"name01" | "name02" | "name03", string>;
// 一般的なオブジェクトとして型定義するケース(今回は使用しません)
type SampleObjectType = {
name01: string;
name02: string;
name03: string;
};
const Union: FC = () => {
const sampleObject: SampleObjectRecord = {
name01: "東京",
name02: "大阪",
name03: "北海道",
} as const;
// sampleObjectのキー(name01, name02, name03)をユニオン型として取得する
const logFunction = (name: keyof typeof sampleObject) => {
console.log(sampleObject[name]);
};
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<Button onClick={() => logFunction("name01")}>東京を出力</Button>
<Button onClick={() => logFunction("name02")}>大阪を出力</Button>
<Button onClick={() => logFunction("name03")}>北海道を出力</Button>
</div>
);
};
export default Union;
上記のコードから生成される画面上のボタンを上から順にクリックすることでconsole.logにvalue値が出力されています。
そのため、関数内でユニオン型を使う場合は、型ファイルのオブジェクトを直接参照し、keyof typeofを活用してユニオン型を生成する方法の方がシンプルで可読性が高いと考えています。この方法なら、型や定数の参照先が関数内に集約されるため、コードを追いやすく、変更やメンテナンスも容易になります。
keyof typeofを使用する上でハマったポイント
TypeScriptの「typeof」とJavaScriptの「typeof」は違うということ
- JavaScriptの「typeof」は値のデータ型を取得するための演算子であり、型を「文字列」で「返します」。
const num = 42;
console.log(typeof num); // "number"
const str = "hello";
console.log(typeof str); // "string"
const obj = { name: "Alice" };
console.log(typeof obj); // "object"
- TypeScriptのtypeofは型システム内で使われる機能で、変数やオブジェクトの「型情報」を取得するために使用されます。配列やオブジェクトの場合、それぞれ型の推論結果が異なるため注意が必要です。
※注意: TypeScriptのtypeofが「型を取得する」と表現され、「返す」とは言われない理由は、TypeScriptが静的型付けのツールであり、実行時に動作するものではないためです。
const obj = { name: "Alice", age: 30 };
type ObjType = typeof obj;
// 推論結果:
// ObjType = {
// name: string;
// age: number;
// }
const num = 1;
type numType = typeof num;
// 推論結果:
// numType = number
const arr = [1, "aoki", true];
type ArrType = typeof arr;
// 推論結果:
// ArrType = (number | string | boolean)[]
// 配列型として推論され、要素は number, string, boolean のいずれか
TypeScriptは、開発時にコードの型を静的に解析し、エラーを検出するためのツールです。
TypeScriptのtypeofはコンパイル時に型情報を取得するために使われます。
一方、JavaScript内で使用するtypeofは、実行時に動作し、値の型を文字列として返します。
したがって、型定義で使うtypeofはTypeScript独自の機能であり、型解析の目的で利用されます。
keyofはTypeScript独自のものである
keyofは、「型情報のキー名をユニオン型として取得するため」に使われる
type Sample = {
name: string;
age: number;
};
type Keys = keyof Sample;
// Keys = "name" | "age"
JavaScriptではObject.keys()を使用することで、キー名の一覧を取得することができる。
これらを踏まえて「keyof typeof」の組み合わせは何をしているのかというと
- まずtypeofが実行されて、オブジェクトの型を取得します。
- 次に取得されたオブジェクトの型情報に対してkeyofが実行されて、ユニオン型が生成されます。
- この生成されるユニオン型は、取得対象としたオブジェクトのキー名を文字列としたものです。
分割して書いたケース
const sampleObject = {
name01: "東京",
name02: "大阪",
name03: "北海道",
};
// 1. typeofで型情報を取得
type SampleObjectType = typeof sampleObject;
// SampleObjectType = {
// name01: string;
// name02: string;
// name03: string;
// }
// 2. keyofでキーをユニオン型として抽出
type Keys = keyof SampleObjectType;
// Keys = "name01" | "name02" | "name03"
ちょっと疑問に思ったこと
「あらかじめ定義してあるオブジェクトに対して、直接keyofを実行すればいいのでは?」と思った。
結論:❌(できない)
なぜできないのか?
keyofはTypeScript独自の機能であり、型にしか作用しないためです。
「値」であるオブジェクトに対しては適用できません。
再度補足
TypeScriptのkeyofは型システム内でのみ動作し、JavaScriptには存在しない機能です。
個人的な考え
関数の引数でユニオン型を使う際の考え方です。
外部ファイルで型や定数を管理し、定数オブジェクトを参照して型定義ディレクトリ内でユニオン型を作成する方法もありますが、このやり方には可読性の面で少し疑問を感じます。
特定のオブジェクトからユニオン型を生成する場合、都度keyof typeof オブジェクト名を使うほうが、型がコードと密接に関連付けられるため可読性が高くなり、レビューもしやすいと考えています。これにより、型定義がコードの近くに存在し、変更やメンテナンスが容易になります。
補足)ドット記法(オブジェクト.キー名)とブラケット記法(オブジェクト["キー名"])に関すること。
- ドット記法は「定義済みのプロパティにアクセスする際」に使われます。プロパティ名が変数や動的に決定される場合は使用できません。
- ブラケット記法(オブジェクト["キー名"])は、プロパティ名が文字列リテラル、数値、変数、または計算式で動的に決定される場合に使用します。なので今回のkeyof typeofではブラケット記法を使用しました。
この辺りを意味を持って使い分けることで、可読性につながるのでは無いかと思います。
最後に
keyofがTS専用のものっていうことは知らなかったですが....
こうやってアウトプットを重ねることで得られる新たな発見もあるなぁと思いました。
ありがとうございました。