Menu

TypeScript typeof:类型守卫与 typeof 类型运算符

typeof 在 TypeScript 中有两种用途。在代码中,它是 JavaScript 运算符,运行时返回 "string"、"number"、"object" 等字符串,TypeScript 会据此收窄类型。在类型中,它是类型查询,用来复制一个变量的类型,例如 keyof typeof obj 和 ReturnType<typeof fn>。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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;两者都无法检查接口或类型别名,因为它们在运行时并不存在。

Coddy 编程语言插图

和 Coddy 一起学编程

开始