TypeScriptの typeof には2つの意味があります。式の中ではJavaScriptの演算子で、実行されて "string" や "number" のような文字列を返し、その文字列を比較するとTypeScriptが変数を絞り込みます。型の位置では型クエリで、typeof config は変数 config の静的な型を返します。
最後の行が違いを示しています。console.log の中の typeof custom は実行時の演算子なので object を出力しますが、type Options = のあとの typeof defaults はコンパイル時のクエリで、JavaScriptには残りません。
2つの意味の比較
実行時の typeof(JavaScript) | 型クエリの typeof(TypeScript) | |
|---|---|---|
| 書く場所 | あらゆる式: if、return、console.log(...) | 型の位置: : のあと、type X = ... の中、<...> の中 |
| 例 | typeof x === "string" | let y: typeof x; |
| 評価されるとき | コードの実行時 | コンパイラーが評価し、その後消える |
| 結果 | 8種類の文字列のどれか | 型 |
| 用途 | 値をチェックして型を絞り込む | 値から型を作る |
TypeScriptは書かれた位置で2つを区別するので、同じキーワードが同時に両方の意味になることはありません。
実行時の typeof の結果
実行時の演算子は8種類の文字列のどれかを返します。よく使う値で何が返るか、実行して確かめてください:
| 値 | typeof の結果 | 備考 |
|---|---|---|
"hi"、テンプレート文字列 | "string" | |
42、3.14、NaN、Infinity | "number" | NaN は数値。Number.isNaN でチェックする |
10n | "bigint" | |
true、false | "boolean" | |
undefined | "undefined" | |
Symbol("id") | "symbol" | |
| 関数、アロー関数、クラス | "function" | クラスはコンストラクター関数 |
null | "object" | JavaScriptの歴史的なバグ |
{}、[]、new Date()、new Map() | "object" | 配列もオブジェクト。Array.isArray を使う |
new String("x")、new Number(1) | "object" | ラッパーオブジェクト。使わない |
素のJavaScriptでは、宣言されていない変数に typeof を使うと、例外を投げずに "undefined" を返します。TypeScriptではそのコードはコンパイルできません: Cannot find name 'notDeclared'.(TS2304)。
typeof による絞り込み
if、switch、三項演算子、&& のあとで typeof を比較すると、各ブランチで変数が絞り込まれます。プリミティブのユニオン型を扱う標準的な方法で、unknown に対して最初に使うチェックです。
typeof x !== "string" は逆方向に、string 以外のすべてに絞り込みます。また、コンパイラーは8種類の文字列を知っているので、比較の文字列を打ち間違えると、黙って常に false になるチェックではなくコンパイルエラーになります:
コンパイラーは index.ts(3,7): error TS2367: This comparison appears to be unintentional because the types '"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"' and '"strnig"' have no overlap. を出力します。素のJavaScriptなら、この打ち間違いで文字列はすべて黙って number の経路に進んでしまいます。つづりを直すと text が出力されます。
typeof null の落とし穴
typeof null は "object" なので、TypeScriptは typeof x === "object" を object ではなく object | null に絞り込みます。null のチェックを追加しないと、プロパティを読むのはエラーです(TS18047 'x' is possibly 'null'.)。
typeof では配列、日付、Map、普通のオブジェクトを区別できません。どれも "object" です。配列には Array.isArray(x)、クラスのインスタンスには x instanceof Date を使います。インターフェースは消去されるので、値がインターフェースに一致するかを実行時にチェックする方法は typeof にもほかの何にもありません。その場合はプロパティをチェックする型ガード関数を書きます。
型の位置の typeof
型の中の typeof someVariable は、その変数についてTypeScriptが推論した(または宣言された)型をコピーします。すでにある値の型を手で書く手間が省け、値と型がずれることもありません。
const theme = {
primary: "#3178c6",
spacing: 8,
dark: false,
};
type Theme = typeof theme;
// { primary: string; spacing: number; dark: boolean }
function withSpacing(t: Theme, factor: number): Theme {
return { ...t, spacing: t.spacing * factor };
}
console.log(withSpacing(theme, 2)); // { primary: '#3178c6', spacing: 16, dark: false }
let userName = "Ada";
const fixed = "Ada";
type A = typeof userName; // string
type B = typeof fixed; // "Ada" (a const keeps its literal type)
結果は変数の宣言のしかたによって変わります。let やオブジェクトのプロパティは広げられ(string、number)、const のプリミティブはリテラル型のままです。オブジェクトや配列に as const を付けると、typeof は奥まですべて readonly のリテラル型になります。typeof はプロパティアクセスも受け付けます(typeof theme.spacing は number)が、任意の式は受け付けません。type T = typeof getTheme(); は構文エラー(';' expected.、TS1005)です。呼び出しが返す型には ReturnType<typeof getTheme> を使います。
keyof typeof: オブジェクトのキーを型にする
keyof typeof obj はいちばんよく使う組み合わせです。typeof がオブジェクトを型に変え、keyof がそのキーを取り出すので、オブジェクトが変わっても正しいままのプロパティ名のユニオン型が得られます。
定数の配列に同じ方法を使うと、値のユニオン型が得られます。const roles = ["admin", "editor"] as const なら、型 (typeof roles)[number] は "admin" | "editor" です。as const がなければただの string になります。keyof 単体については keyof のページで説明しています。
ReturnType<typeof fn> とクラスの typeof
ReturnType や Parameters のようなユーティリティ型は関数の型を受け取ります。関数名は値なので、先に typeof を通します:
ReturnType<createUser> と書くと 'createUser' refers to a value, but is being used as a type here. Did you mean 'typeof createUser'?(TS2749)で失敗します。コンパイラーがまさにこの書き方を求めているわけです。
名前が値でもあり型でもある唯一のケースがクラスです。型としての User はクラスのインスタンスを意味し、typeof User はクラスそのもの、つまり static メンバーを含むコンストラクターを意味します:
class Point {
static origin = new Point(0, 0);
constructor(public x: number, public y: number) {}
}
const p: Point = new Point(1, 2); // an instance
const Ctor: typeof Point = Point; // the class (constructor + statics)
const q = new Ctor(3, 4); // q: Point
type Instance = InstanceType<typeof Point>; // Point
typeof、instanceof、型ガードの比較
| チェック | 対象 | 例 |
|---|---|---|
typeof | プリミティブ、関数、「オブジェクトかどうか」 | typeof x === "number" |
Array.isArray | 配列 | Array.isArray(x) |
instanceof | クラスのインスタンス(Date、Error、自作のクラス) | x instanceof Date |
in | プロパティによるオブジェクトのユニオン型 | "email" in x |
| 型ガード関数 | インターフェース、型エイリアス、その他すべて | x is User を返す isUser(x) |
条件に合う、いちばん限定的な方法を選びましょう。クラスのインスタンスについては instanceof を参照してください。インターフェースで表した形については、使える実行時のチェックは型ガード関数だけです。
よくある質問
TypeScriptの typeof は何をしますか?
書く場所によって2つの違うことをします。式の中の typeof x はJavaScriptの演算子で、実行されて "string" や "object" のような文字列を返し、その文字列を比較するとTypeScriptが x を絞り込みます。型注釈の中の typeof x は型クエリで、コンパイラーが評価して変数 x の静的な型を返し、出力されるJavaScriptからは消えます。
TypeScriptの keyof typeof とは何ですか?
keyof typeof obj は、オブジェクトのプロパティ名を文字列リテラル型のユニオンとして返します。typeof obj がオブジェクトの値を型に変え、keyof がその型のキーを取り出します。const colors = { red: "#f00", blue: "#00f" } なら、keyof typeof colors は "red" | "blue" です。
typeof null が "object" を返すのはなぜですか?
JavaScriptの最初のバージョンからあるバグで、Webを壊さずに直すことはもうできません。TypeScriptはこれを再現しています。typeof x === "object" のあとでは x は object | null に絞り込まれるので、プロパティを読む前に x !== null もチェックする必要があります。
TypeScriptで値が配列かどうかをチェックするには?
Array.isArray(value) を使います。これで配列型に絞り込まれます。typeof ではできません。typeof [] は、普通のオブジェクトや null と同じく "object" です。
TypeScriptの typeof と instanceof の違いは何ですか?
typeof は値のプリミティブの種類("string"、"number"、"function"、"object" など)をチェックします。instanceof は、オブジェクトが Date や自分で作った class User のような特定のクラスやコンストラクターから作られたかをチェックします。プリミティブには typeof、クラスのインスタンスには instanceof を使います。どちらも実行時に存在しないインターフェースや型エイリアスはチェックできません。