typeof 在 TypeScript 中有两种含义。在表达式中,它是 JavaScript 运算符:它会运行并返回 "string" 或 "number" 这样的字符串,当你比较这个字符串时,TypeScript 会收窄变量的类型。在类型位置,它是类型查询:typeof config 得到变量 config 的静态类型。
最后一行展示了两者的区别:console.log 中的 typeof custom 是运行时运算符,输出 object;而 type Options = 后面的 typeof defaults 是编译期查询,永远不会出现在 JavaScript 中。
两种含义一览
运行时 typeof(JavaScript) | 类型查询 typeof(TypeScript) | |
|---|---|---|
| 出现的位置 | 任何表达式:if、return、console.log(...) | 类型位置:: 之后、type X = ... 中、<...> 里 |
| 示例 | typeof x === "string" | let y: typeof x; |
| 何时求值 | 代码运行时 | 由编译器求值,随后擦除 |
| 结果 | 八个字符串之一 | 一个类型 |
| 用途 | 检查值并收窄它的类型 | 从值推导出类型 |
TypeScript 根据位置区分两者,所以同一个关键字永远不会同时表示两种含义。
typeof 的运行时结果
运行时运算符返回八个字符串之一。运行下面的代码,看看常见的值会得到什么:
| 值 | 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 的历史遗留 bug |
{}、[]、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 之外的所有类型。而且编译器知道这八个可能的字符串,所以比较中的拼写错误会成为编译错误,而不是一个悄悄永远为假的检查:
编译器输出 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 | null,而不是 object。需要再加一个 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 就会一路得到只读的字面量类型。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 则表示类本身,也就是构造函数,包括它的静态成员:
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 有什么作用?
根据出现的位置,它做两件不同的事。在表达式中,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 第一个版本留下的 bug,如今已经无法在不破坏现有网页的情况下修复。TypeScript 如实地模拟了它:在 typeof x === "object" 之后,x 被收窄为 object | null,所以读取属性之前还必须检查 x !== null。
在 TypeScript 中如何判断一个值是不是数组?
使用 Array.isArray(value),它会把类型收窄为数组类型。typeof 做不到:typeof [] 是 "object",和普通对象以及 null 一样。
TypeScript 中 typeof 和 instanceof 有什么区别?
typeof 检查值的基本类别("string"、"number"、"function"、"object"...)。instanceof 检查一个对象是否由某个类或构造函数创建,例如 Date 或你自己的 class User。原始类型用 typeof,类的实例用 instanceof;两者都无法检查接口或类型别名,因为它们在运行时并不存在。