TypeScript 特殊类型
1. any
当值指定为 any
类型时表示该值不会被类型系统检查,也可以说这个值可以是任何类型,没有任何限制。
TS 将 any 类型称为“顶层类型”(top level),即是其他所有类型的全集
any 类型的变量可以赋予任何类型的值,也可以被赋予任何类型的变量:
let x: any;
x = 1; // 正确
x = true; // 正确
let y: string;
y = x; // 正确
2
3
4
5
6
实际开发中应该尽量避免使用 any 类型,否则就是去了使用 TS 的意义。
默认配置下未指定类型且无法被推断的值就会被识别为 any 类型:
function add(x, y) {
// x和y都是any类型
return x + y;
}
// 不会包错
add(true, [1, 2, 3]);
2
3
4
5
6
如果开启了 TS 提供了的 noImplicitAny
编译选项,那么未指定类型且无法识别的值就会报错:
// Parameter 'x' implicitly has an 'any' type.
// Parameter 'y' implicitly has an 'any' type.
function add(x, y) {
return x + y;
}
2
3
4
5
但是有个例外是使用 var
或 let
声明变量但不指定值是不会报错的:
var x; // 不报错
let y; // 不报错
2
所以在使用 let
和 var
时建议显式声明类型。
any 类型处理不检查类型之外,还有个问题是会“污染”其他变量,因为 any 可以赋值给其他任何类型的变量,这可能导致其他变量出错。
let x: any = "hello";
let y: number;
y = x; // 不报错
y * 123; // 不报错
y.toFixed(); // 不报错
2
3
4
5
6
7
上面的示例中因为 x 是 any 类型,所以代码不会有类型报错,但是实际运行时却会出现错误,这也是不宜使用 any 类型的另一个主要原因。
2. unknown
TS 在 3.0 版本引入了 unknown
类型,表示“不确定”类型。它与 any 类似可以是任意类型,但是无法像 any 一样被自由分配和调用。
let x: unknown;
// unknown 和 any 一样可以赋值为任意类型
x = true; // 不报错
x = 42; // 不报错
// 但是不能直接使用
let y: boolean = x; // 报错
x.toString(); // 报错
2
3
4
5
6
7
unknown 能够进行的运算是有限的,只能进行比较运算(运算符==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符!
)、typeof
运算符和instanceof
运算符这几种,其他运算都会报错。
let a: unknown = 1;
a + 1; // 报错
a === 1; // 正确
2
3
4
正确使用 unknown 类型的方法是经过“类型收窄”,也就是将不确定的类型确定为具体的类型,来保证代码不会出错,例如:
let a: unknown = 1;
if (typeof a === "number") {
a++; // 正确
}
2
3
4
5
上面的代码通过 typeof 运算确定 a 是数字类型,可以保证后续代码不会出错。
unknown 提供了一个不确定的类型,这样设计的目的是填补 any 类型可以随意使用的问题。在集合论上,unknown 也可以视为所有其他类型(除了 any)的全集,所以它和 any 一样,也属于 TypeScript 的顶层类型。
3. never
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型” never
,即该类型为空,不包含任何值。
let x: never;
never 类型的变量不能赋予任何值,否则都会报错。
never 类型的主要使用场景主要再一些类型运算中,后续会进行介绍。出现 never 的情况大致有:
- 不可能返回值的函数(例如逻辑报错或死循环永远无法运行结束),那函数的返回类型就可以写为 never
function foo(): never {
throw new Error();
}
function foo(): never {
while (true) {}
}
2
3
4
5
6
7
- 联合类型通过分支处理了每一种可能,那剩余的情况就是 never
function fn(x: string | number) {
if (typeof x === "string") {
// ...
} else if (typeof x === "number") {
// ...
} else {
x; // never 类型
}
}
2
3
4
5
6
7
8
9
never 类型的一个重要特点是,可以赋值给任意其他类型,因为 never 代表空集,是其他任何集合的子集,在 TS 中把这种情况称为“底层类型”(bottom type)。
function f(): never {
throw new Error("Error");
}
let v1: number = f(); // 不报错
2
3
4
5
4. void
void
类型表示函数没有返回值,但允许返回 undefined
和 null
(如果开启了 strictNullChecks
则只允许返回 undefined
),如果返回其他值则会报错。
function f(): void {
return undefined; // 正确
}
function f(): void {
return null; // 正确
}
function f(): void {
return 123; // 报错
}
2
3
4
5
6
7
8
9
10
11
需要特别注意的是,如果变量、对象方法、函数参数是一个返回值为 void
类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。
const f: () => void = () => {
return 123;
};
2
3
这是因为,TS 认为 void 类型只是代表函数返回值没有利用价值,不会被使用到所以返回或者不返回都不会报错。
函数的运行结果如果是抛出错误,也允许将返回值写成 void。
function throwErr(): void {
throw new Error("something wrong");
}
2
3
除了函数,其他变量声明为 void 类型没有多大意义,因为这时只能赋值为 undefined
或者 null
(如果没有打开strictNullChecks
)。
let foo: void = undefined;
// 没有打开 strictNullChecks 的情况下
let bar: void = null;
2
3
4
5. 元组
元组(tuple)是 TS 特有的数据类型,本质是就是每个成员类型固定的数组类型:
const s: [string, string, boolean] = ["a", "b", true];
上面的 s 就是一个元组,它定义了每一个成员的类型,赋值时也必须遵守成员类型规则,每个成员与类型一一对应。TS 中元组和数组的区分方式就是,成员类型写在方括号里面的就是元组,写在外面的就是数组。
声明元组时必须给出类型声明,否则会被推断为数组。元组成员的类型可以在后端添加 ?
后缀,表示该成员是可选的,但是带有 ?
后缀的成员后面不能再有必须的成员,规则与 JS 函数的可选参数规则类似。
const s: [string, string, boolean?, number?] = ["a", "b"];
由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。但是,使用扩展运算符 ...
,可以表示不限成员数量的元组。扩展运算符可以用在元组的任意位置,它的后面只能是一个数组或元组:
let x: [string, string] = ["a", "b"];
x[2] = "c"; // 报错
let y: [string, ...number[]] = ["a", 1, 2];
y[3] = 3; // 正确
2
3
4
5
元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用。
const c: [red: number, green: number, blue: number] = [255, 255, 255];
元组也可以是只读的,语法为 readonly [type]
。和数组一样,只读元组是元组的父类型,所以元组可以代替只读元组,但只读元组不能代替元组:
let x: readonly [number, number] = [1, 2];
let y: [number, number] = x; // 正确
x = y; // 报错
function foo(arg: [number, number]) {}
distanceFromOrigin(x); // 报错
2
3
4
5
6
7
使用 as const
语法生成的只读数组也可以看作只读元组,因为它生成的实际上是一个只读值类型 readonly [value]
:
let point = [3, 4] as const;
如果没有可选成员和扩展运算符,TS 会推断出元组准确的长度;如果包含了可选成员,TS 会推断出元组可能的长度;如果使用了扩展运算符,TS 则无法推断出元组长度:
function a(point: [number, number]) {
if (point.length === 3) {
// 报错
}
}
function b(point: [number, number?, number?]) {
if (point.length === 4) {
// 报错
}
}
function c(point: [number, ...number[]]) {
if (point.length === 9) {
// 正确
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
扩展运算符(...
)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时 TS 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的。
这导致如果函数调用时,使用扩展运算符传入一个数组作为函数参数,可能发生参数数量与数组长度不匹配的报错,此时可以将数组类型改为元组类型解决。
const a = [1, 2];
const b: [number, number] = [1, 2];
function add(x: number, y: number) {}
add(...a); // 报错
add(...b); // 正确
2
3
4
5
6
7
6. Enum
实际开发中经常需要定义一组相关的常量,TS 就设计了 Enum 结构,称为枚举类型,用于将相关常量放在一个容器中。使用时与调用对象属性一样:
enum Gender {
male, // 0
female, // 1
}
let m = Gender.male;
let f = Gender.female;
2
3
4
5
6
7
上面的示例中,第一个成员的值默认为 0,第二个为 1,以此类推。Enum 本身也是一种类型,例如上面的变量 m
和 f
,它们的类型可以是 Gender 也可以是 number。Enum 结构的特别之处就在于,它既是一种类型,也是一个值。绝大多数的 TS 语法都是类型语法,编译后会被去除,但是 Enum 编译后会变成 JS 对象,留在代码中:
// 编译前
enum Gender {
male, // 0
female, // 1
}
// 编译后
var Gender;
(function (Gender) {
Gender[(Gender["male"] = 0)] = "male";
Gender[(Gender["female"] = 1)] = "female";
})(Gender || (Gender = {}));
// 等同于
var Gender = {
0: "male",
1: "female",
male: 0,
female: 1,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
由于 TS 的定位是 JS 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
TS 5.0 之前,Enum 有一个 Bug,就是 Enum 类型的变量可以赋值为任何数值:
enum Bool {
No,
Yes,
}
function foo(noYes: Bool) {}
foo(33); // TypeScript 5.0 之前不报错
2
3
4
5
6
7
8
另外,由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等):
enum Color {
Red,
Green,
Blue,
}
const Color = "red"; // 报错
2
3
4
5
6
7
很大程度上,Enum 结构可以被对象的 as const
断言替代:
enum Foo {
A,
B,
C,
}
const Bar = {
A: 0,
B: 1,
C: 2,
} as const;
if (x === Foo.A) {
}
// 等同于
if (x === Bar.A) {
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Enum 成员的值默认会从 0 开始递增,但是也可以显式赋值。成员值可以是任意数值包括计算语句,但不能是 bigint;成员值甚至可以相同,如果只设置了一个成员的值,后面未赋值的成员会从这个值开始递增:
enum A {
A = 10,
B = 1 << 2, // 正确
C = Math.random(), // 正确
D = 7n, // bigint值报错
}
enum B {
A = 0,
B = 0, // 不报错
}
enum C {
A = 9,
B = 90,
C, // 值为91
D = 8,
E, // 值为9
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Enum 成员的值都是只读的,不能重新赋值。为了让这一点更醒目,通常会在 enum 关键字前面加上 const 修饰,表示这是常量,不能再次赋值。
const enum Color {
Red,
Green,
Blue,
}
Color.Red = 4; // 报错
2
3
4
5
6
7
加上 const 还有一个好处,就是编译为 JS 代码后,代码中 Enum 成员会被替换成对应的值(前文提到的编译后的立即执行函数代码将不再存在),这样能提高性能表现:
const enum Color {
Red,
Green,
Blue,
}
const x = Color.Red;
const y = Color.Green;
const z = Color.Blue;
// 编译后
const x = 0; /* Color.Red */
const y = 1; /* Color.Green */
const z = 2; /* Color.Blue */
2
3
4
5
6
7
8
9
10
11
12
13
14
如果希望加上 const 关键词后,运行时还能访问 Enum 结构(即编译后依然将 Enum 转成对象),需要在编译时打开preserveConstEnums
编译选项。
多个同名的 Enum 结构会自动合并,但是合并时,只允许其中一个的首成员省略初始值,否则报错;多个同名 Enum 不能有同名成员,否则报错。另外合并的枚举定义必须同为 const 枚举或者非 const 枚举,不允许混合使用:
enum Foo {
A,
}
enum Foo {
B = 1,
}
enum Foo {
C = 2,
}
// 等同于
// enum Foo {
// A,
// B = 1,
// C = 2
// }
enum Foo {
D, // 报错
}
enum Foo {
A = 1, // 报错
}
const enum Foo {
E = 3, // 报错
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Enum 成员值除了允许设为数值,还可以设为字符串,且可以混合赋值。但是字符串枚举成员后的成员必须显示赋值,也就是无法从字符串枚举成员开始推断默认值,会报错:
enum Foo {
A, // 0
B = "hello",
C = 6,
D, // 7
E = "word",
F, // 报错
}
2
3
4
5
6
7
8
除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样:
enum MyEnum {
One = "One",
Two = "Two",
}
let a = MyEnum.One;
a = "One"; // 报错
let b: MyEnum.One = "one";
a = b; // 正确
2
3
4
5
6
7
8
9
上文提到了 Enum 编译后的代码会对 Enum 进行“反向映射”,即将值:名
的关系也写入编译后的对象中。但是字符串枚举不存在反向映射,因为字符串 Enum 编译后只有一组赋值:
// 编译前
enum Foo {
A, // 0
B = "hello",
}
// 编译后
var Foo;
(function (Foo) {
Foo[(Foo["A"] = 0)] = "A";
Foo["B"] = "hello";
})(Foo || (Foo = {}));
2
3
4
5
6
7
8
9
10
11