为什么会有字面量类型
先来做个小小的测试题,你能准确猜出来,下面 TS 代码中变量的类型吗?
const companyName = "Shanshu";
let companyArea = "beijing";
var companyPeopleCount = 200;
等待十秒中……
答案揭晓:
const companyName = "Shanshu"; // Shanshu
let companyArea = "beijing"; // name
var companyPeopleCount = 200; // number
解释下,因为 TS 的类型推断,TS 会把能改变(var 和 let 声明的变量)的变量 companyArea 和 companyPeopleCount 自动推断为合适的类型,const 声明的常量如果赋值为普通类型,因为其永远不会再改变了,则推断为字面量类型。
字面量类型的用处
在我们编程中,除了 const 声明的变量,一个变量只有一个值的意义并不是很大,字面量真正发挥威力是作为联合类型使用。
那使用字面量类型的好处有啥呢,我认为好处有两个:
- 更加直观,程序不易出错
- 细化 TS 类型系统
第一个好处,举例:
function printText(s: string, alignment: "left" | "right" | "center") {
// ... do something
}
printText("Hello, world", "center");
想象一个函数的第二个参数,如果没有使用字面量类型,那只能用 string 类型,一旦使用 string 类型,意味着我们能传入任何字符串,万一第二个参数拼写错误,printText("Hello, world", "centre");
程序的鲁棒性就不复存在了。
第二点好处,举例:
玩过 UI 框架都知道,table column 的 width 可以指定为 number 和 string。
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 }); // YES
configure("auto"); // YES
configure("automatic"); // NO
想象一下如果没有字面量类型,我们只能这么写 x: Options | string
把 auto 变成 string,这样的话 configure("automatic");
就是对的了,容错性增强了,但程序的鲁棒性降低了。
字面量类型推论
当我们使用 const 变量初始化一个对象字面量类型的时候,例如 :
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
因为对象的 counter 属性可能会被更改,所以 obj.counter
的类型会默认被推论为 number,而不是字面量类型 0,(PS:如果你想让 counter 属性就是字面类型 0,也是可以的,下面会介绍)。
知道这个特性,我们接下里在看一个例子。
相信大家在项目中都发送过 Ajax/Fetch 请求,而且为了参数便于管理,我们一般会把发送的参数放入一个对象里面,就像下面这样。
function handleRequest(url: string, method: "GET" | "POST") {
// do something...
}
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'
在 TS 中 req.method 会报错,这是因为 method 参数指定的类型是字面量类型 "GET" | "POST",而 const 声明的字面量类型 req 对象的 method 会自动推论为 string,所以会报错。
那应该怎么解决呢?
使用断言,断言为字面量类型
- 类型断言
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");
- const 类型断言(推荐这个)
这里的 const 不是声明变量的关键字,而是 const 类型。
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
它会让 const 声明的变量变成真正的字面量类型,我们看下不加 const 断言 req 变量的类型推论:
下面是加上 const 断言的 req 变量的类型。
模版字面量类型
我们知道字面量类型,可以为数字、布尔值、字符串借助断言甚至可以是复杂数据类型的字面量。
不过接下里我们重点说说字符串字面量。提到字符串有一个难题就是字符串的拼接问题,JS 最优雅的字符串拼接,你一定很熟了,那就是模版字符串。
字符串字面量类型本质还是字符串,也会遇到拼接问题,那怎么搞呢?对,你没猜错 模版字面量类型(Template Literal Types),不过对 TS 版本要求是大于等于 4.1 。
我们看看模版字面量类型怎么使用,我现在需要一个类型变量 Greeting 是 hello world!
,我可以这么做:
type Hello = "hello";
type World = "world";
type Greeting = `${Hello} ${World}!`;
演示结果:
TS 的模版字面量类型的用法很简单和 JS 的模版字符串使用基本一致。
字符串字面量类型四个方法
JS 字符串有两个很实用的方法 toLowerCase 和 toUpperCase,分别负责把字符串全部变小写和全部变大写。字符串字面量类型本质作为字符串也有这两种方法,而且还多了两个,一种四个,不过在 TS 中应该叫泛型,而不是方法。
Uppercase
把每个字符串都转换成大写。
type NickName = "CondorHero-2021";
// "CONDORHERO-2021"
type UppercaseNickName = Uppercase<NickName>;
Lowercase
把每个字符串都转换成小写。
type NickName = "CONDORHERO-2021";
// "condorhero-2021"
type LowercaseNickName = Lowercase<NickName>;
Capitalize
把首字符变成大写。
type NickName = "condorhero-2021";
// "Condorhero-2021"
type CapitalizeNickName = Capitalize<NickName>;
Uncapitalize
把首字符变成小写。
type NickName = "CONDORHERO-2021";
// "cONDORHERO-2021"
type UncapitalizeNickName = Uncapitalize<NickName>;
这时候有个问题,这四个泛型的源码怎么实现呢?
来来,我们来看看,见证时刻:
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
intrinsic 是什么呢?说好的源码呢。原来 intrinsic 的意思代表这四个泛型是通过 TS 编译器来实现的。
不满足这个答案的你肯定会问底层到底怎么实现的呢?其实也很简单通过 JS 来实现的,我们看下代码:
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}
好了,到此关于字面量类型的内容就结束了。