TypeScript 是一种类型安全的 JavaScript 超集,除了基本类型和对象类型之外,TypeScript 还提供了一些高级类型系统,使得我们可以更好地处理复杂的数据结构和业务逻辑。本文将深入探讨 TypeScript 的高级类型系统,以更好地理解和使用这些高级类型,提高代码的可读性、可维护性和健壮性。
1. 字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即字面量类型。TypeScript 支持以下字面量类型:
- 字符串字面量类型;
- 数字字面量类型;
- 布尔字面量类型;
- 模板字面量类型。
(1)字符串字面量类型
字符串字面量类型其实就是字符串常量,与字符串类型不同的是它是具体的值:
type Name = "TS";
const name1: Name = "test"; // ❌ 不能将类型“"test"”分配给类型“"TS"”。ts(2322)
const name2: Name = "TS";
实际上,定义单个字面量类型在实际应用中并没有太大的用处。它的应用场景就是将多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合:
type Direction = "north" | "east" | "south" | "west";
function getDirectionFirstLetter(direction: Direction) {
return direction.substr(0, 1);
}
getDirectionFirstLetter("test"); // ❌ 类型“"test"”的参数不能赋给类型“Direction”的参数。
getDirectionFirstLetter("east");
这个例子中使用四个字符串字面量类型组成了一个联合类型。这样在调用函数时,编译器就会检查传入的参数是否是指定的字面量类型集合中的成员。通过这种方式,可以将函数的参数限定为更具体的类型。这不仅提升了代码的可读性,还保证了函数的参数类型。
除此之外,使用字面量类型还可以为我们提供智能的代码提示:
(2)数字字面量类型
数字字面量类型就是指定类型为具体的数值:
type Age = 18;
interface Info {
name: string;
age: Age;
}
const info: Info = {
name: "TS",
age: 28 // ❌ 不能将类型“28”分配给类型“18”
};
可以将数字字面量类型分配给一个数字,但反之是不行的:
let val1: 10|11|12|13|14|15 = 10;
let val2 = 10;
val2 = val1;
val1 = val2; // ❌ 不能将类型“number”分配给类型“10 | 11 | 12 | 13 | 14 | 15”。
(3)布尔字面量类型
布尔字面量类型就是指定类型为具体的布尔值(true
或false
):
let success: true;
let fail: false;
let value: true | false;
success = true;
success = false; // ❌ 不能将类型“false”分配给类型“true”
由于布尔字面量类型只有true或false两种,所以下面 value 变量的类型是一样的:
let value: true | false;
let value: boolean;
(4)模板字面量类型
在 TypeScript 4.1 版本中新增了模板字面量类型。什么是模板字面量类型呢?它一字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。它与 JavaScript 的模板字符串语法相同,但是只能用在类型定义中使用。
① 基本语法
当使用模板字面量类型时,它会替换模板中的变量,返回一个新的字符串字面量。
type attrs = "Phone" | "Name";
type target = `get${attrs}`;
// type target = "getPhone" | "getName";
可以看到,模板字面量类型的语法简单,并且易读且功能强大。
假如有一个CSS内边距规则的类型,定义如下:
type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom';
上面的类型是没有问题的,但是有点冗长。margin 和 padding 的规则相同,但是这样写我们无法重用任何内容,最终就会得到很多重复的代码。
下面来使用模版字面量类型来解决上面的问题:
type Direction = 'left' | 'right' | 'top' | 'bottom';
type CssPadding = `padding-${Direction}`
// type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom'
这样代码就变得更加简洁。如果想创建margin类型,就可以重用Direction类型:
type CssMargin = `margin-${Direction}`
如果在 JavaScript 中定义了变量,就可以使用 typeof 运算符来提取它:
const direction = 'left';
type CssPadding = `padding-${typeof direction}`;
// type CssPadding = "padding-left"
② 变量限制
模版字面量中的变量可以是任意的类型吗?可以使用对象或自定义类型吗?来看下面的例子:
type CustomObject = {
foo: string
}
type target = `get${CustomObject}`
// ❌ 不能将类型“CustomObject”分配给类型“string | number | bigint | boolean | null | undefined”。
type complexUnion = string | number | bigint | boolean | null | undefined;
type target2 = `get${complexUnion}` // ✅
可以看到,当在模板字面量类型中使用对象类型时,就报错了,因为编译器不知道如何将它序列化为字符串。实际上,模板字面量类型中的变量只允许是string、number、bigint、boolean、null、undefined或这些类型的联合类型。
③ 实用程序
Typescript 提供了一组实用程序来帮助处理字符串。它们不是模板字面量类型独有的,但与它们结合使用时很方便。完整列表如下:
- Uppercase<StringType>:将类型转换为大写。
- Lowercase<StringType>:将类型转换为小写。
- Capitalize<StringType>:将类型第一个字母大写。
- Uncapitalize<StringType>:将类型第一个字母小写。
这些实用程序只接受一个字符串字面量类型作为参数,否则就会在编译时抛出错误:
复制
type nameProperty = Uncapitalize<'Name'>;
// type nameProperty = 'name';
type upercaseDigit = Uppercase<10>;
// ❌ 类型“number”不满足约束“string”。
type property = 'phone';
type UppercaseProperty = Uppercase<property>;
// type UppercaseProperty = 'Property';
下面来看一个更复杂的场景,将字符串字面量类型与这些实用程序结合使用。将两种类型进行组合,并将第二种类型的首字母大小,这样组合之后的类型符合驼峰命名法:
type actions = 'add' | 'remove';
type property = 'name' | 'phone';
type result = `${actions}${Capitalize<property>}`;
// type result = addName | addPhone | removeName | removePhone
④ 类型推断
在上面的例子中,我们使用使用模版字面量类型将现有的类型组合成新类型。下面来看看如何使用模板字面量类型从组合的类型中提取类型。这里就需要用到infer关键字,它允许我们从条件类型中的另一个类型推断出一个类型。
下面来尝试提取字符串字面量 marginRight 的根节点:
type Direction = 'left' | 'right' | 'top' | 'bottom';
type InferRoot<T> = T extends `${infer K}${Capitalize<Direction>}` ? K : T;
type Result1 = InferRoot<'marginRight'>;
// type Result1 = 'margin';
type Result2 = InferRoot<'paddingLeft'>;
// type Result2 = 'padding';
可以看到, 模版字面量还是很强大的,不仅可以创建类型,还可以解构它们。
⑤ 作为判别式
在TypeScript 4.5 版本中,支持了将模板字面量串类型作为判别式,用于类型推导。来看下面的例子:
interface Message {
type: string;
url: string;
}
interface SuccessMessage extends Message {
type: `${string}Success`;
body: string;
}
interface ErrorMessage extends Message {
type: `${string}Error`;
message: string;
}
function handler(r: SuccessMessage | ErrorMessage) {
if (r.type === "HttpSuccess") {
let token = r.body;
}
}
在这个例子中,handler 函数中的 r 的类型就被推断为 SuccessMessage。因为根据 SuccessMessage 和 ErrorMessage 类型中的type字段的模板字面量类型推断出 HttpSucces 是根据SuccessMessage中的type创建的。
2. 联合类型
(1)基本使用
联合类型是一种互斥的类型,该类型同时表示所有可能的类型。联合类型可以理解为多个类型的并集。 联合类型用来表示变量、参数的类型不是某个单一的类型,而可能是多种不同的类型的组合,它通过 | 来分隔不同的类型:
type Union = "A" | "B" | "C";
在编写一个函数时,该函数的期望参数是数字或者字符串,并根据传递的参数类型来执行不同的逻辑。这时就用到了联合类型:
function direction(param: string | number) {
if (typeof param === "string") {
...
}
if (typeof param === "number") {
...
}
...
}
这样在调用 direction 函数时,就可以传入string或number类型的参数。当联合类型比较长或者想要复用这个联合类型的时候,就可以使用类型别名来定义联合类型:
type Params = string | number | boolean;
再来看一个字符串字面量联合类型的例子,setStatus 函数只能接受某些特定的字符串值,就可以将这些字符串字面量组合成一个联合类型:
type Status = 'not_started' |