Typescript 中更多的字面量类型

2020-10-11

Typescript 1.8 引入了字符串字面量类型,用于将变量的值限定在有限的字符串字面量集合中。在 Typescript 2.0 中,字面量类型不再局限于字符串字面量。以下的这些字面量类型被添加到了类型系统中:

  • 布尔字面量类型
  • 数字字面量类型
  • 枚举字面量类型

下面我们就每一种新的字面量类型举一个实际的例子。

布尔字面量类型

下面的代码中定义了两个常量,TRUEFALSE,他们分别保存了 truefalse 两个值:

const TRUE: true = true; // OK
const FALSE: false = false; // OK

如果你尝试给这两个变量赋值相反的布尔值,会报类型错误:

const TRUE: true = false;
// Error: Type 'false' is not assignable to type 'true'

const FALSE: false = true;
// Error: Type 'true' is not assignable to type 'false'

在引入布尔字面量类型后,预定义的boolean类型现在等同于 true | false 这个联合类型:

let value: true | false; // Type boolean

布尔字面量类型单独使用可能并没有太多用处,但和标签联合类型(tagged union types)以及基于控制流的类型分析(control flow based types analysis) 配合非常有用。举个例子,Result<T> 泛型要么有一个 T 类型的值,要么包含了一个 string 类型的错误信息,它被定义如下:

type Result<T> =
  | { success: true; value: T }
  | { success: false; error: string };

这儿有个函数使用了这个泛型:

function parseEmailAddress(
  input: string | null | undefined
): Result<string> {
  // If the input is null, undefined, or the empty string
  // (all of which are falsy values), we return early.
  if (!input) {
    return {
      success: false,
      error: "The email address cannot be empty."
    };
  }

  // We're only checking that the input matches the pattern
  //   <something> @ <something> DOT <something>
  // to keep it simple. Properly validating email addresses
  // via regex is hard, so let's not even try here.
  if (!/^\S+@\S+\.\S+$/.test(input)) {
    return {
      success: false,
      error: "The email address has an invalid format."
    };
  }

  // At this point, control flow based type analysis
  // has determined that the input has type string.
  // Thus, we can assign input to the value property.
  return {
    success: true,
    value: input
  };
}

注意strictNullChecks配置选项开启后,string类型是non-nullable类型。为了使我们的函数能够接收 nullable 类型的参数,nullundefined 类型必须显式地添加到联合类型中。

我们现在可以像下面这样调用 parseEmailFunction 函数:

const parsed = parseEmailAddress("example@example.com");

if (parsed.success) {
  parsed.value; // OK
  parsed.error; // Error
} else {
  parsed.value; // Error
  parsed.error; // OK
}

下面是以上代码片段在 VSCode 中的截图。注意,有一些属性访问表达式被标注了红色的下划线:

这里面值得一提的是,在我们检查了parsed.success这个可辨识属性(discriminant property)之后,编译器只允许我们访问 value 或者 error 属性:

  • 如果parsed.successtrueparsed肯定是{ success: true; value: string } 类型。因此我们可以访问value属性,但不能访问 error
  • 如果parsed.successfalseparsed肯定是{ success: false; error: string } 类型。因此我们可以访问error属性,但不能访问 value

顺便提一句,你发现了吗,这段代码中和 Typescript 相关的只有 Result<T> 和函数签名的类型标注?其他的部分都是普通的、原汁原味的 Javascript,而这些代码因为基于控制流的类型分析而得到了类型安全。

数字字面量类型

和字符串字面量类似,我们可以将一个数字类型变量取值限定于已知的一个有限集合中:

let zeroOrOne: 0 | 1;

zeroOrOne = 0;
// OK

zeroOrOne = 1;
// OK

zeroOrOne = 2;
// Error: Type '2' is not assignable to type '0 | 1'

举个例子,在实际使用中,我们可以用数字字面量类型来定义端口号。HTTP 默认使用 80 端口,HTTPS 默认使用 443 端口。我们可以写一个 getPort 函数,并且在函数签名中声明只能返回这两个可能的值:

function getPort(scheme: "http" | "https"): 80 | 443 {
  switch (scheme) {
    case "http":
      return 80;
    case "https":
      return 443;
  }
}

const httpPort = getPort("http"); // Type 80 | 443

更有趣的是,如果我们结合 Typescript 中的函数重载,会得到更具体的类型:

function getPort(scheme: "http"): 80;
function getPort(scheme: "https"): 443;
function getPort(scheme: "http" | "https"): 80 | 443 {
  switch (scheme) {
    case "http":
      return 80;
    case "https":
      return 443;
  }
}

const httpPort = getPort("http"); // Type 80
const httpsPort = getPort("https"); // Type 443

如下图所示,当我们去比较httpPort443,编译器会提示我们这个条件语句会永远返回 false:

既然 httpPort 的类型是 80,那么它永远只能包含 80 的值,而 80 显然永远不会等于 443。类似这样的案例,Typescript 编译器可以帮助你检测有 bugger 的代码和一些不会执行的死代码(dead code)。

枚举字面量类型

最后,我们还能使用枚举字面量类型。继续我们上面的例子,我们将实现一个函数用来映射给定的端口(80 或 443)到相应的协议(HTTP 或 HTTPS)。首先,我们来定义一个枚举常量,包含了这两个端口号:

const enum HttpPort {
  Http = 80,
  Https = 443
}

现在我们来声明我们的 getScheme 函数,再一次,我们使用函数重载来限定特定类型:

function getScheme(port: HttpPort.Http): "http";
function getScheme(port: HttpPort.Https): "https";
function getScheme(port: HttpPort): "http" | "https" {
  switch (port) {
    case HttpPort.Http:
      return "http";
    case HttpPort.Https:
      return "https";
  }
}

const scheme = getScheme(HttpPort.Http);

枚举常量不会产生运行时的代码(除非你提供了 preserveConstEnums 编译器配置)。也就是说在编译之后,枚举常量的值会被直接内联到代码中。下面就是编译后的 Javascript 代码:

function getScheme(port) {
  switch (port) {
    case 80:
      return "http";
    case 443:
      return "https";
  }
}
var scheme = getScheme(80);

是不是很简洁?

郑超的独立博客