TypeScript——类型基础(一)

类型基础

1、类型注解

在TypeScript中,我们可以使用类型注解来明确标识类型。类型注解的语法由一个冒号":"和某种具体类型"Type"组成,示例如下:

ts 复制代码
:Type

TypeScript中的类型注解总是放在被修饰的实体之后。示例如下:

ts 复制代码
const greeting: string = 'Hello, World!';

此例中,我们为常量greeting添加了类型注解,将它标记成了string类型。

TypeScript中的类型注解是可选的,编译器在大部分情况下都能够自动推断出表达式的类型。示例如下:

ts 复制代码
const greeting = 'Hello, World!';

此例中,虽然没有给常量greeting添加类型注解,但是TypeScript仍然能够从greeting的初始值中推断出它是string类型的常量。

2、类型检查

类型检查是验证程序中类型约束是否正确的过程。类型检查既可以在程序编译时进行,即静态类型检查;也可以在程序运行时进行,即动态类型检查。TypeScript支持静态类型检查,JavaScript支持动态类型检查。

为了满足不同用户的需求,TypeScript提供了两种静态类型检查模式:

  • 非严格类型检查(默认方式)。
  • 严格类型检查。

2.1、非严格类型检查

非严格类型检查是TypeScript默认的类型检查模式。在该模式下,类型检查的规则相对宽松。例如,在非严格类型检查模式下不会对undefined值和null值做过多限制,允许将undefined值和null值赋值给string类型的变量。当进行JavaScript代码到TypeScript代码的迁移工作时,非严格类型检查是一个不错的选择,因为它能够让我们快速地完成迁移工作。

2.2、严格类型检查

该模式下的类型检查比较激进,会尽可能地发现代码中的错误。例如,在严格类型检查模式下不允许将undefined值和null值赋值给string类型的变量。启用严格类型检查模式能够最大限度地利用TypeScript静态类型检查带来的益处。从长远来讲,使用严格类型检查模式对提高代码质量更加有利,因此建议在新的工程中启用严格类型检查。

TypeScript提供了若干个与严格类型检查相关的编译选项,例如"--strictNullChecks"和"--noImplicitAny"等。

在学习TypeScript语言的过程中,推荐启用所有严格类型检查编译选项。如果使用TypeScript官网提供的在线代码编辑器,那么这些严格类型检查编译选项是默认开启的。如果使用本地开发环境,那么可以在工程的tsconfig.json配置文件中启用"--strict"编译选项。示例如下:

ts 复制代码
{
    "compilerOptions": {
        "strict": true,
    }
}

此例中,将"--strict"编译选项设置为true将开启所有的严格类型检查编译选项。它包含了前面提到的"--strictNullChecks"和"--noImplicitAny"编译选项。

3、原始类型

到目前为止,TypeScript中的原始类型包含以下几种:

  • boolean
  • string
  • number
  • bigint
  • symbol
  • undefined
  • null
  • void
  • 枚举类型
  • 字面量类型

3.1、boolean

TypeScript中的boolean类型对应于JavaScript中的Boolean原始类型。该类型能够表示两个逻辑值:true和false。

boolean类型使用boolean关键字来表示。示例如下:

ts 复制代码
const yes: boolean = true;
const no: boolean = false;

3.2、String

TypeScript中的string类型对应于JavaScript中的String原始类型。该类型能够表示采用Unicode UTF-16编码格式存储的字符序列。

string类型使用string关键字表示。我们通常使用字符串字面量或模板字面量来创建string类型的值。示例如下:

ts 复制代码
const foo: string = 'foo';
const bar: string = `bar, ${foo}`;

3.3、number

TypeScript中的number类型对应于JavaScript中的Number原始类型。该类型能够表示采用双精度64位二进制浮点数格式存储的数字。

number类型使用number关键字来表示。示例如下:

ts 复制代码
// 二进制数
const bin: number = 0b1010;

// 八进制数
const oct: number = 0o744;

// 十进制数
const integer: number = 10;
const float: number = 3.14;

// 十六进制数
const hex: number = 0xffffff;

3.4、bigint

TypeScript中的bigint类型对应于JavaScript中的BigInt原始类型。该类型能够表示任意精度的整数,但也仅能表示整数。bigint采用了特殊的对象数据结构来表示和存储一个整数。

bigint类型使用bigint关键字来表示。示例如下:

ts 复制代码
// 二进制整数
const bin: bigint = 0b1010n;

// 八进制整数
const oct: bigint = 0o744n;

// 十进制整数
const integer: bigint = 10n;

// 十六进制整数
const hex: bigint = 0xffffffn;

3.5、symbol与unique symbol

TypeScript中的symbol类型对应于JavaScript中的Symbol原始类型。该类型能够表示任意的Symbol值。

symbol类型使用symbol关键字来表示。示例如下:

ts 复制代码
// 自定义Symbol
const key: symbol = Symbol();

// Well-Known Symbol
const symbolHasInstance: symbol = Symbol.hasInstance;

字面量能够表示一个固定值。例如,数字字面量"3"表示固定数值"3"​;字符串字面量"'up'"表示固定字符串"'up'"​。symbol类型不同于其他原始类型,它不存在字面量形式。symbol类型的值只能通过"Symbol()"和"Symbol.for()"函数来创建或直接引用某个"Well-Known Symbol"值。示例如下:

ts 复制代码
const s0: symbol = Symbol();
const s1: symbol = Symbol.for('foo');
const s2: symbol = Symbol.hasInstance;
const s3: symbol = s0;

为了能够将一个Symbol值视作表示固定值的字面量,TypeScript引入了"unique symbol"类型。​"unique symbol"类型使用"uniquesymbol"关键字来表示。示例如下:

ts 复制代码
const s0: unique symbol = Symbol();
const s1: unique symbol = Symbol.for('s1');

"unique symbol"类型的主要用途是用作接口、类等类型中的可计算属性名。因为如果使用可计算属性名在接口中添加了一个类型成员,那么必须保证该类型成员的名字是固定的,否则接口定义将失去意义。下例中,允许将"unique symbol"类型的常量x作为接口的类型成员,而symbol类型的常量y不能作为接口的类型成员,因为symbol类型不止包含一个可能值:

ts 复制代码
const x: unique symbol = Symbol();
const y: symbol = Symbol();

interface Foo {
    [x]: string; // 正确

    [y]: string;
//  ~~~
//  错误:接口中的计算属性名称必须引用类型为字面量类型
//  或'unique symbol'的表达式
}
```实际上,​"unique symbol"类型的设计初衷是作为一种变通方法,让一个Symbol值具有字面量的性质,即仅表示一个固定的值。​"unique symbol"类型没有改变Symbol值没有字面量表示形式的事实。为了能够将某个Symbol值视作表示固定值的字面量,TypeScript对"unique symbol"类型和Symbol值的使用施加了限制。

TypeScript选择将一个Symbol值与声明它的标识符绑定在一起,并通过绑定了该Symbol值的标识符来表示"Symbol字面量"​。这种设计的前提是要确保Symbol值与标识符之间的绑定关系是不可变的。因此,TypeScript中只允许使用const声明或readonly属性声明来定义"unique symbol"类型的值。示例如下:
```ts
// 必须使用const声明
const a: unique symbol = Symbol();

interface WithUniqueSymbol {
    // 必须使用readonly修饰符
    readonly b: unique symbol;
}

class C {
    // 必须使用static和readonly修饰符
    static readonly c: unique symbol = Symbol();
}

此例第1行,常量a的初始值为Symbol值,其类型为"uniquesymbol"类型。在标识符a与其初始值Symbol值之间形成了绑定关系,并且该关系是不可变的。这是因为常量的值是固定的,不允许再被赋予其他值。标识符a能够固定表示该Symbol值,标识符a的角色相当于该Symbol值的字面量形式。

如果使用let或var声明定义"unique symbol"类型的变量,那么将产生错误,因为标识符与Symbol值之间的绑定是可变的。示例如下:

ts 复制代码
let a: unique symbol = Symbol();
//  ~
//  错误:'unique symbol' 类型的变量必须使用'const'

var b: unique symbol = Symbol();
//  ~
//  错误:'unique symbol' 类型的变量必须使用'const'

"unique symbol"类型的值只允许使用"Symbol()"函数或"Symbol.for()"方法的返回值进行初始化,因为只有这样才能够"确保"引用了唯一的Symbol值。示例如下:

ts 复制代码
const a: unique symbol = Symbol();
const b: unique symbol = Symbol('desc');

const c: unique symbol = a;
//    ~
//    错误:a的类型与c的类型不兼容

const d: unique symbol = b;
//    ~
//    错误:b的类型与d的类型不兼容

但是,我们知道使用相同的参数调用"Symbol.for()"方法实际上返回的是相同的Symbol值。因此,可能出现多个"unique symbol"类型的值实际上是同一个Symbol值的情况。由于设计上的局限性,TypeScript目前无法识别出这种情况,因此不会产生编译错误,开发者必须要留意这种特殊情况。示例如下:

ts 复制代码
const a: unique symbol = Symbol.for('same');
const b: unique symbol = Symbol.for('same'); 

此例中,编译器会认为a和b是两个不同的Symbol值,而实际上两者是相同的。

在设计上,每一个"unique symbol"类型都是一种独立的类型。在不同的"unique symbol"类型之间不允许相互赋值;在比较两个"unique symbol"类型的值时,也将永远返回false。示例如下:

ts 复制代码
const a: unique symbol = Symbol();
const b: unique symbol = Symbol();

if (a === b) {
//  ~~~~~~~
//  该条件永远为false

    console.log('unreachable code');
}

由于"unique symbol"类型是 symbol类型的子类型,因此可以将"unique symbol"类型的值赋值给symbol类型。示例如下:

ts 复制代码
const a: unique symbol = Symbol();

const b: symbol = a;

如果程序中未使用类型注解来明确定义是symbol类型还是"uniquesymbol"类型,那么TypeScript会自动地推断类型。示例如下:

ts 复制代码
// a和b均为'symbol'类型,因为没有使用const声明
let a = Symbol();
let b = Symbol.for('');

// c和d均为'unique symbol'类型
const c = Symbol();
const d = Symbol.for('');

// e和f均为'symbol'类型,没有使用Symbol()或Symbol.for()初始化
const e = a;
const f = a;

3.6、Nullable

TypeScript中的Nullable类型指的是值可以为undefined或null的类型。

JavaScript中有两个比较特殊的原始类型,即Undefined类型和Null类型。两者分别仅包含一个原始值,即undefined值和null值,它们通常用来表示某个值还未进行初始化。

在TypeScript早期的版本中,没有提供与JavaScript中Undefined类型和Null类型相对应的类型。TypeScript允许将undefined值和null值赋值给任何其他类型。虽然在TypeScript语言的内部实现中确实存在这两种原始类型,但是之前没有将它们开放给开发者使用。

现在,在TypeScript程序中能够明确地指定某个值的类型是否为undefined类型或null类型。TypeScript编译器也能够对代码进行更加细致的检查以找出程序中潜在的错误。

1. undefined

undefined类型只包含一个可能值,即undefined值。undefined类型使用undefined关键字标识。示例如下:

ts 复制代码
const foo: undefined = undefined;

2. null

null类型只包含一个可能值,即null值。null类型使用null关键字标识。示例如下:

ts 复制代码
const foo: null = null;

3. --strictNullChecks

TypeScript 2.0还增加了一个新的编译选项"--strictNullChecks"​,即严格的null检查模式。虽然该编译选项的名字中只提及了null,但实际上它同时作用于undefined类型和null类型的类型检查。

在默认情况下,​"--strictNullChecks"编译选项没有被启用。这时候,除尾端类型外的所有类型都是Nullable类型。也就是说,除尾端类型外所有类型都能够接受undefined值和null值。

例如,在没有启用"--strictNullChecks"编译选项时,允许将undefined值和null值赋值给string类型等其他类型。示例如下:

ts 复制代码
/**
 * --strictNullChecks=false
 */
let m1: boolean   = undefined;
let m2: string    = undefined;
let m3: number    = undefined;
let m4: bigint    = undefined;
let m5: symbol    = undefined;
let m6: undefined = undefined;
let m7: null      = undefined;

let n1: boolean   = null;
let n2: string    = null;
let n3: number    = null;
let n4: bigint    = null;
let n5: symbol    = null;
let n6: undefined = null;
let n7: null      = null; 

该模式存在一个明显的问题,就是无法检查出空引用的错误。例如,已知某一个变量的类型是string,于是通过访问其length属性来获取该变量表示的字符串的长度。但如果string类型的变量值可以为undefined或null,那么这段代码在运行时将产生错误。示例如下:

ts 复制代码
/**
 * --strictNullChecks=false
 */
let foo: string = undefined; // 正确,可以通过类型检查

foo.length;                  // 在运行时,将产生类型错误

// 运行结果:
// Error: TypeError: Cannot read property 'length'
// of undefined

此例中,将undefined值赋值给string类型的变量foo时不会产生编译错误。但是,在运行时尝试读取undefined值的length属性将产生类型错误。这个问题可以通过启用"--strictNullChecks"编译选项来避免。

当启用了"--strictNullChecks"编译选项时,undefined值和null值不再能够赋值给不相关的类型。例如,undefined值和null值不允许赋值给string类型。在该模式下,undefined值只能够赋值给undefined类型;同理,null值也只能赋值给null类型。

还是以上例中的代码为例,如果我们启用了"--strictNullChecks"编译选项,那么TypeScript编译器就能够检查出代码中的错误。示例如下:

ts 复制代码
/**
 * --strictNullChecks=true
 */
let foo: string = undefined;
//  ~~~
//  编译错误!类型 'undefined' 不能赋值给类型 'string'

foo.length;

此例第4行,TypeScript在执行静态类型检查时就能够发现这处类型错误,从而避免了在代码运行时才发现这个缺陷。

前面我们说在启用了"--strictNullChecks"编译选项时,undefined值只能够赋值给undefined类型,null值只能够赋值给null类型,实际上这种表述不完全准确。因为在该模式下,undefined值和null值允许赋值给顶端类型,同时undefined值也允许赋值给void类型。这些类型在后面的章节中会有详细介绍。示例如下:

ts 复制代码
/**
 * --strictNullChecks=true
 */
let m1: void = undefined;

let m2: any     = undefined;
let m3: unknown = undefined;

let n2: any     = null;
let n3: unknown = null;

undefined类型和null类型是不同的类型,它们必须被区分对待,不能互换使用。示例如下:

ts 复制代码
/**
 * --strictNullChecks=true
 */
const foo: undefined = null;
//    ~~~
//    编译错误!类型 'null' 不能赋值给类型 'undefined'

const bar: null = undefined;
//    ~~~
//    编译错误!类型 'undefined' 不能赋值给类型 'null'

在了解了"--strictNullChecks"编译选项的作用后,让我们来看一看如何启用该编译选项。在默认情况下,​"--strictNullChecks"编译选项没有被启用,我们需要在工程下的tsconfig.json配置文件中启用该编译选项,通过将"strictNullChecks"属性设置为"true"就能够启用"--strictNullChecks"编译选项。同理,如果将该属性设置为"false"则会关闭该编译选项

json 复制代码
{
    "compilerOptions": {         	
    	"strictNullChecks": true
    }
}

3.7、void

void类型表示某个值不存在,该类型用作函数的返回值类型。若一个函数没有返回值,那么该函数的返回值类型为void类型。除了将void类型作为函数返回值类型外,在其他地方使用void类型是无意义的。

ts 复制代码
function log(message: string): void {
    console.log(message);
}

此例中,log函数的参数类型为string,返回值类型为void,表示该函数"没有"返回值。

当启用了"--strictNullChecks"编译选项时,只允许将undefined值赋值给void类型。示例如下:

ts 复制代码
/**
 * --strictNullChecks=true
 */

// 正确
function foo(): void {
    return undefined;
}

// 编译错误!类型 'null' 不能赋值给类型 'void'
function bar(): void {
    return null;
}

如果没有启用"--strictNullChecks"编译选项,那么允许将undefined值和null值赋值给void类型。

4、枚举类型

枚举类型由零个或多个枚举成员构成,每个枚举成员都是一个命名的常量。

在TypeScript中,枚举类型是一种原始类型,它通过enum关键字来定义。例如,我们可以使用枚举类型来表示一年四季,示例如下:

ts 复制代码
enum Season {
    Spring,
    Summer,
    Fall,
    Winter,
}

按照枚举成员的类型可以将枚举类型划分为以下三类:

  • 数值型枚举
  • 字符串枚举
  • 异构型枚举

4.1、数值型枚举

数值型枚举是最常用的枚举类型,是number类型的子类型,它由一组命名的数值常量构成。定义数值型枚举的方法如下所示:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right
}

const direction: Direction = Direction.Up;

此例中,我们使用enum关键字定义了枚举类型Direction,它包含了四个枚举成员Up、Down、Left和Right。在使用枚举成员时,可以像访问对象属性一样访问枚举成员。

每个数值型枚举成员都表示一个具体的数字。如果在定义枚举时没有设置枚举成员的值,那么TypeScript将自动计算枚举成员的值。根据TypeScript语言的规则,第一个枚举成员的值为0,其后每个枚举成员的值等于前一个枚举成员的值加1。因此,Direction枚举中Up的值为0、Down的值为1,以此类推。示例如下:

ts 复制代码
enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right,   // 3
}

在定义数值型枚举时,可以为一个或多个枚举成员设置初始值。对于未指定初始值的枚举成员,其值为前一个枚举成员的值加1。

ts 复制代码
enum Direction {
    Up = 1,    // 1
    Down,      // 2
    Left = 10, // 10
    Right,     // 11
}

数值型枚举是number类型的子类型,因此允许将数值型枚举类型赋值给number类型。例如,下例中常量direction为number类型,可以使用数值型枚举Direction来初始化direction常量。示例如下:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right
}

const direction: number = Direction.Up;

需要注意的是,number类型也能够赋值给枚举类型,即使number类型的值不在枚举成员值的列表中也不会产生错误。示例如下:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

const d1: Direction = 0;  // Direction.Up
const d2: Direction = 10; // 不会产生错误

4.2、字符串枚举

字符串枚举与数值型枚举相似。在字符串枚举中,枚举成员的值为字符串。字符串枚举成员必须使用字符串字面量或另一个字符串枚举成员来初始化。字符串枚举成员没有自增长的行为。示例如下:

ts 复制代码
enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',

    U = Up,
    D = Down,
    L = Left,
    R = Right,
}

字符串枚举是string类型的子类型,因此允许将字符串枚举类型赋值给string类型。例如,下例中常量direction为string类型,可以使用字符串枚举Direction来初始化direction常量:

ts 复制代码
enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

const direction: string = Direction.Up;

但是反过来,不允许将string类型赋值给字符串枚举类型,这一点与数值型枚举是不同的。例如,下例中将字符串"'UP'"赋值给字符串枚举类型的常量direction将产生编译错误:

ts 复制代码
enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

const direction: Direction = 'UP';
//    ~~~~~~~~~
//    编译错误!类型 'UP' 不能赋值给类型 'Direction'

4.3、异构型枚举

TypeScript允许在一个枚举中同时定义数值型枚举成员和字符串枚举成员,我们将这种类型的枚举称作异构型枚举。异构型枚举在实际代码中很少被使用,虽然在语法上允许定义异构型枚举,但是不推荐在代码中使用异构型枚举。我们可以尝试使用对象来代替异构型枚举。

ts 复制代码
enum Color {
    Black = 0,
    White = 'White',
}

在定义异构型枚举时,不允许使用计算的值作为枚举成员的初始值。示例如下:

ts 复制代码
enum Color {
    Black = 0 + 0,
    //      ~~~~~
    //      编译错误!在带有字符串成员的枚举中不允许使用计算值

    White = 'White',
}

在异构型枚举中,必须为紧跟在字符串枚举成员之后的数值型枚举成员指定一个初始值。下例中,ColorA枚举的定义是正确的,但是ColorB枚举的定义是错误的,必须为数值型枚举成员Black指定一个初始值。示例如下:

ts 复制代码
enum ColorA {
    Black,
    White = 'White',
}

enum ColorB {
    White = 'White',
    Black,
//  ~~~~~
//  编译错误!枚举成员必须有一个初始值
}

4.4、枚举成员映射

不论是哪种类型的枚举,都可以通过枚举成员名去访问枚举成员值。下例中,通过枚举名Bool和枚举成员名False与True能够访问枚举成员的值:

ts 复制代码
enum Bool {
    False = 0,
    True = 1,
}

Bool.False;   // 0
Bool.True;    // 1

对于数值型枚举,不但可以通过枚举成员名来获取枚举成员值,也可以反过来通过枚举成员值去获取枚举成员名。下例中,通过枚举成员值"Bool.False"能够获取其对应的枚举成员名,即字符串"'False'"​:

ts 复制代码
enum Bool {
    False = 0,
    True = 1,
}

Bool[Bool.False]; // 'False'
Bool[Bool.True];  // 'True'

对于字符串枚举和异构型枚举,则不能够通过枚举成员值去获取枚举成员名。

4.5、常量枚举成员与计算枚举成员

每个枚举成员都有一个值,根据枚举成员值的定义可以将枚举成员划分为以下两类:

  • 常量枚举成员
  • 计算枚举成员

1. 常量枚举成员:

若枚举类型的第一个枚举成员没有定义初始值,那么该枚举成员是常量枚举成员并且初始值为0。示例如下:

ts 复制代码
enum Foo {
    A,   // 0
}

若枚举成员没有定义初始值并且与之紧邻的前一个枚举成员值是数值型常量,那么该枚举成员是常量枚举成员并且初始值为紧邻的前一个枚举成员值加1。如果紧邻的前一个枚举成员的值不是数值型常量,那么将产生错误。示例如下:

ts 复制代码
enum Foo {
    A,        // 0
    B,        // 1
}

enum Bar {
    C = 'C',
    D,        // 编译错误
}

若枚举成员的初始值是常量枚举表达式,那么该枚举成员是常量枚举成员。常量枚举表达式是TypeScript表达式的子集,它能够在编译阶段被求值。常量枚举表达式的具体规则如下:

  • 常量枚举表达式可以是数字字面量、字符串字面量和不包含替换值的模板字面量。
  • 常量枚举表达式可以是对前面定义的常量枚举成员的引用。
  • 常量枚举表达式可以是用分组运算符包围起来的常量枚举表达式。
  • 常量枚举表达式中可以使用一元运算符"+""-""~",操作数必须为常量枚举表达式。
  • 常量枚举表达式中可以使用二元运算符"+""-""*""**""/""%""<<"">>"">>>""&""|""^",两个操作数必须为常量枚举表达式。
ts 复制代码
enum Foo {
    A = 0,           // 数字字面量
    B = 'B',         // 字符串字面量
    C = `C`,         // 无替换值的模板字面量
    D = A,           // 引用前面定义的常量枚举成员
}

enum Bar {
    A = -1,          // 一元运算符
    B = 1 + 2,       // 二元运算符
    C = (4 / 2) * 3, // 分组运算符(小括号)
}

字面量枚举成员是常量枚举成员的子集。字面量枚举成员是指满足下列条件之一的枚举成员,具体条件如下:

  • 枚举成员没有定义初始值。
  • 枚举成员的初始值为数字字面量、字符串字面量和不包含替换值的模板字面量。
  • 枚举成员的初始值为对其他字面量枚举成员的引用。

下例中,Foo枚举的所有成员都是字面量枚举成员,同时它们也都是常量枚举成员:

ts 复制代码
enum Foo {
    A,
    B = 1,
    C = -3,
    D = 'foo',
    E = `bar`,
    F = A
}

2. 计算枚举成员:

除常量枚举成员之外的其他枚举成员都属于计算枚举成员。下例中,枚举成员"Foo.A"和"Foo.B"均为计算枚举成员:

ts 复制代码
enum Foo {
    A = 'A'.length,
    B = Math.pow(2, 3)
}

3. 使用示例:

枚举表示一组有限元素的集合,并通过枚举成员名来引用集合中的元素。有时候,程序中并不关注枚举成员值。在这种情况下,让TypeScript去自动计算枚举成员值是很方便的。示例如下:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

function move(direction: Direction) {
    switch (direction) {
        case Direction.Up:
            console.log('Up');
            break;
        case Direction.Down:
            console.log('Down');
            break;
        case Direction.Left:
            console.log('Left');
            break;
        case Direction.Right:
            console.log('Right');
            break;
    }
}

move(Direction.Up);   // 'Up'
move(Direction.Down); // 'Down'

程序不依赖枚举成员值时,能够降低代码耦合度,使程序易于扩展。例如,我们想给Direction枚举添加一个名为None的枚举成员来表示未知方向。按照惯例,None应作为第一个枚举成员。因此,我们可以将代码修改如下:

ts 复制代码
enum Direction {
    None,
    Up,
    Down,
    Left,
    Right,
}

function move(direction: Direction) {
    switch (direction) {
        case Direction.None:
            console.log('None');
            break;
        case Direction.Up:
            console.log('Up');
            break;
        case Direction.Down:
            console.log('Down');
            break;
        case Direction.Left:
            console.log('Left');
            break;
        case Direction.Right:
            console.log('Right');
            break;
    }
}

move(Direction.Up);   // 'Up'
move(Direction.Down); // 'Down'
move(Direction.None); // 'None'

此例中,枚举成员Up、Down、Left和Right的值已经发生了改变,Up的值由0变为1,以此类推。由于move()函数的行为不直接依赖枚举成员的值,因此本次代码修改对move()函数的已有功能不产生任何影响。但如果程序中依赖了枚举成员的具体值,那么这次代码修改就会破坏现有的代码。

4.6、联合枚举类型

当枚举类型中的所有成员都是字面量枚举成员时,该枚举类型成了联合枚举类型。

1. 联合枚举成员类型:

联合枚举类型中的枚举成员除了能够表示一个常量值外,还能够表示一种类型,即联合枚举成员类型。

下例中,Direction枚举是联合枚举类型,Direction枚举成员Up、Down、Left和Right既表示数值常量,也表示联合枚举成员类型:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

const up: Direction.Up = Direction.Up;

此例第8行,第一个"Direction.Up"表示联合枚举成员类型,第二个"Direction.Up"则表示数值常量0。

联合枚举成员类型是联合枚举类型的子类型,因此可以将联合枚举成员类型赋值给联合枚举类型。示例如下:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

const up: Direction.Up = Direction.Up;

const direction: Direction = up;

此例中,常量up的类型是联合枚举成员类型"Direction.Up"​,常量direction的类型是联合枚举类型Direction。由于"Direction.Up"类型是Direction类型的子类型,因此可以将常量up赋值给常量direction。

2. 联合枚举类型:

联合枚举类型是由所有联合枚举成员类型构成的联合类型。示例如下:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

type UnionDirectionType =
    | Direction.Up
    | Direction.Down
    | Direction.Left
    | Direction.Right;

此例中,Direction枚举是联合枚举类型,它等同于联合类型UnionDirectionType,其中"|"符号是定义联合类型的语法。

由于联合枚举类型是由固定数量的联合枚举成员类型构成的联合类型,因此编译器能够利用该性质对代码进行类型检查。示例如下:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

function f(direction: Direction) {
    if (direction === Direction.Up) {
        // Direction.Up
    } else if (direction === Direction.Down) {
        // Direction.Down
    } else if (direction === Direction.Left) {
        // Direction.Left
    } else {
        // 能够分析出此处的direction为Direction.Right
        direction;
    }
}

此例中,编译器能够分析出Direction联合枚举类型只包含四种可能的联合枚举成员类型。在"if-else"语句中,编译器能够根据控制流分析出最后的else分支中direction的类型为"Direction.Right"​。

下面再来看另外一个例子。Foo联合枚举类型由两个联合枚举成员类型"Foo.A"和"Foo.B"构成。编译器能够检查出在第7行if条件判断语句中的条件表达式结果永远为true,因此将产生编译错误。示例如下:

ts 复制代码
enum Foo {
    A = 'A',
    B = 'B',
}

function bar(foo: Foo) {
    if (foo !== Foo.A || foo !== Foo.B) {
        //               ~~~~~~~~~~~~~
        //               编译错误:该条件永远为'true'
    }
}

4.7、const枚举类型

枚举类型是TypeScript对JavaScript的扩展,JavaScript语言本身并不支持枚举类型。在编译时,TypeScript编译器会将枚举类型编译为JavaScript对象。例如,我们定义如下的枚举:

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

const d: Direction = Direction.Up;

此例中的代码编译后生成的JavaScript代码如下所示,为了支持枚举成员名与枚举成员值之间的正、反向映射关系,TypeScript还生成了一些额外的代码:

ts 复制代码
"use strict";
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

const d = Direction.Up;

有时候我们不会使用枚举成员值到枚举成员名的反向映射,因此没有必要生成额外的反向映射代码,只需要生成如下代码就能够满足需求:

ts 复制代码
"use strict";
var Direction;
(function (Direction) {
    Direction["Up"] = 0;
    Direction["Down"] = 1
    Direction["Left"] = 2
    Direction["Right"] = 3
})(Direction || (Direction = {}));

const d = Direction.Up;

更进一步讲,如果我们只关注第10行枚举类型的使用方式就会发现,完全不需要生成与Direction对象相关的代码,只需要将"Direction.Up"替换为它所表示的常量0即可。经过此番删减后的代码量将大幅减少,并且不会改变程序的运行结果,如下所示:

ts 复制代码
"use strict";
const d = 0;

const枚举类型具有相似的效果。const枚举类型将在编译阶段被完全删除,并且在使用了const枚举类型的地方会直接将const枚举成员的值内联到代码中。

const枚举类型使用"const enum"关键字定义,示例如下:

ts 复制代码
const enum Directions {
    Up,
    Down,
    Left,
    Right,
}

const directions = [
    Directions.Up,
    Directions.Down,
    Directions.Left,
    Directions.Right,
];

此例中的代码经过TypeScript编译器编译后生成的JavaScript代码如下所示:

ts 复制代码
"use strict";
const directions = [
    0 /* Up */,
    1 /* Down */,
    2 /* Left */,
    3 /* Right */
];

我们能够注意到,为了便于代码调试和保持代码的可读性,TypeScript编译器在内联了const枚举成员的位置还额外添加了注释,注释的内容为枚举成员的名字。

5、字面量类型

TypeScript支持将字面量作为类型使用,我们称之为字面量类型。每一个字面量类型都只有一个可能的值,即字面量本身。

5.1、boolean字面量类型

boolean字面量类型只有以下两种:

  • true字面量类型。
  • false字面量类型。

原始类型boolean等同于由true字面量类型和false字面量类型构成的联合类型,即:

ts 复制代码
type BooleanAlias = true | false;

true字面量类型只能接受true值;同理,false字面量类型只能接受false值,示例如下:

ts 复制代码
const a: true = true;

const b: false = false;

boolean字面量类型是boolean类型的子类型,因此可以将boolean字面量类型赋值给boolean类型,示例如下:

ts 复制代码
const a: true = true;
const b: false = false;

let c: boolean;
c = a;
c = b;

5.2、string字面量类型

字符串字面量和模板字面量都能够创建字符串。字符串字面量和不带参数的模板字面量可以作为string字面量类型使用。示例如下:

ts 复制代码
const a: 'hello' = 'hello';

const b: `world` = `world`;

string字面量类型是string类型的子类型,因此可以将string字面量类型赋值给string类型。示例如下:

ts 复制代码
const a: 'hello' = 'hello';
const b: `world` = `world`;

let c: string;
c = a;
c = b;

5.3、数字字面量类型

数字字面量类型包含以下两类:

  • number字面量类型。
  • bigint字面量类型。

所有的二进制、八进制、十进制和十六进制数字字面量都可以作为数字字面量类型。示例如下:

ts 复制代码
const a0: 0b1 = 1;
const b0: 0o1 = 1;
const c0: 1 = 1;
const d0: 0x1 = 1;

const a1: 0b1n = 1n;
const b1: 0o1n = 1n;
const c1: 1n = 1n;
const d1: 0x1n = 1n;

除了正数数值外,负数也可以作为数字字面量类型。示例如下:

ts 复制代码
const a0: -10 = -10;
const b0: 10 = 10;

const a1: -10n = -10n;
const b1: 10n = 10n;

number字面量类型和bigint字面量类型分别是number类型和bigint类型的子类型,因此可以进行赋值操作。示例如下:

ts 复制代码
const one: 1 = 1;
const num: number = one;

const oneN: 1n = 1n;
const numN: bigint = oneN;

5.4、枚举成员字面量类型

我们也可以将其称作枚举成员字面量类型,因为联合枚举成员类型使用枚举成员字面量形式表示。示例如下:[插图]

ts 复制代码
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

const up: Direction.Up = Direction.Up;
const down: Direction.Down = Direction.Down;
const left: Direction.Left = Direction.Left;
const right: Direction.Right = Direction.Right;

6、单元类型

单元类型(Unit Type)也叫作单例类型(Singleton Type)​,指的是仅包含一个可能值的类型。由于这个特殊的性质,编译器在处理单元类型时甚至不需要关注单元类型表示的具体值。

TypeScript中的单元类型有以下几种:

  • undefined类型。
  • null类型。
  • unique symbol类型。
  • void类型。
  • 字面量类型。
  • 联合枚举成员类型。

我们能够看到这些单元类型均只包含一个可能值。示例如下:

ts 复制代码
const a: undefined = undefined;
const b: null = null;
const c: unique symbol = Symbol();
const d: void = undefined;
const e: 'hello' = 'hello';

enum Foo { A, B }
const f: Foo.A = Foo.A;

7、顶端类型

顶端类型(Top Type)源自于数学中的类型论,同时它也被广泛应用于计算机编程语言中。顶端类型是一种通用类型,有时也称为通用超类型,因为在类型系统中,所有类型都是顶端类型的子类型,或者说顶端类型是所有其他类型的父类型。顶端类型涵盖了类型系统中所有可能的值。

TypeScript中有以下两种顶端类型:

  • any
  • unknown

7.1、any

any类型是从TypeScript 1.0开始就支持的一种顶端类型。any类型使用any关键字作为标识,示例如下:

ts 复制代码
let x: any;

在TypeScript中,所有类型都是any类型的子类型。我们可以将任何类型的值赋值给any类型。示例如下:

ts 复制代码
let x: any;

x = true;
x = 'hi';
x = 3.14;
x = 99999n;
x = Symbol();
x = undefined;
x = null;
x = {};
x = [];
x = function () {};

需要注意的是,虽然any类型是所有类型的父类型,但是TypeScript允许将any类型赋值给任何其他类型。示例如下:

ts 复制代码
let x: any;

let a: boolean   = x;
let b: string    = x;
let c: number    = x;
let d: bigint    = x;
let e: symbol    = x;
let f: void      = x;
let g: undefined = x;
let h: null      = x;

在any类型上允许执行任意的操作而不会产生编译错误。例如,我们可以读取any类型的属性或者将any类型当作函数调用,就算any类型的实际值不支持这些操作也不会产生编译错误。示例如下:

ts 复制代码
const a: any = 0;

a.length;

a();

a[0];

在程序中,我们使用any类型来跳过编译器的类型检查。如果声明了某个值的类型为any类型,那么就相当于告诉编译器:​"不要对这个值进行类型检查。​"当TypeScript编译器看到any类型的值时,也会对它开启"绿色通道"​,让其直接通过类型检查。在将已有的JavaScript程序迁移到TypeScript程序的过程中,使用any类型来暂时绕过类型检查是一项值得掌握的技巧。示例如下:

ts 复制代码
function parse(data: any) {
    //               ~~~
    //               编译器不检查data参数的类型

    console.log(data.id);
}

从长远来看,我们应该尽量减少在代码中使用any类型。因为只有开发者精确地描述了类型信息,TypeScript编译器才能够更加准确有效地进行类型检查,这也是我们选择使用TypeScript语言的主要原因之一。

--noImplicitAny

TypeScript中的类型注解是可选的。若一个值没有明确的类型注解,编译器又无法自动推断出它的类型,那么这个值的默认类型为any类型。示例如下:

ts 复制代码
function f1(x) {
    //      ~
    //      参数x的类型为any
    console.log(x);
}

function f2(x: any) {
    console.log(x);
}

此例中,函数f1的参数x没有使用类型注解,编译器也无法从代码中推断出参数x的类型。于是,函数f1的参数x将隐式地获得any类型。最终,函数f1的类型等同于函数f2的类型。在这种情况下,编译器会默默地忽略对参数x的类型检查,这会导致编译器无法检查出代码中可能存在的错误。

在大多数情况下,我们想要避免上述情况的发生。因此,TypeScript提供了一个"--noImplicitAny"编译选项来控制该行为。当启用了该编译选项时,如果发生了隐式的any类型转换,那么会产生编译错误。示例如下:

ts 复制代码
function f(x) {
    //     ~
    //     编译错误!参数'x'具有隐式的'any'类型

    console.log(x);
}

此例中,参数x具有隐式的any类型,因此将产生编译错误。我们可以使用如下方式在"tsconfig.json"配置文件中启用"--noImplicitAny"编译选项:

ts 复制代码
{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

7.2、unknown

TypeScript 3.0版本引入了另一种顶端类型unknown。unknown类型使用unknown关键字作为标识。示例如下:

ts 复制代码
let x: unknown;

根据顶端类型的性质,任何其他类型都能够赋值给unknown类型,该行为与any类型是一致的。示例如下:

ts 复制代码
let x: unknown;

x = true;
x = 'hi';
x = 3.14;
x = 99999n;
x = Symbol();
x = undefined;
x = null;
x = {};
x = [];
x = function () {};

unknown类型是比any类型更安全的顶端类型,因为unknown类型只允许赋值给any类型和unknown类型,而不允许赋值给任何其他类型,该行为与any类型是不同的。示例如下:

ts 复制代码
let x: unknown;

// 正确
const a1: any = x;
const b1: unknown = x;

// 错误
const a2: boolean   = x;
const b2: string    = x;
const c2: number    = x;
const d2: bigint    = x;
const e2: symbol    = x;
const f2: undefined = x;
const g2: null      = x;

同时,在unknown类型上也不允许执行绝大部分操作。示例如下:

ts 复制代码
let x: unknown;

// 错误
x + 1;
x.foo;
x();

在程序中使用unknown类型时,我们必须将其细化为某种具体类型,否则将产生编译错误。示例如下:

ts 复制代码
function f1(message: any) {
    return message.length;
    //             ~~~~~~
    //             无编译错误
}

f1(undefined);

function f2(message: unknown) {
    return message.length;
    //             ~~~~~~
    //             编译错误!属性'length'不存在于'unknown'类型上
}

f2(undefined);

此例中,函数f1的参数message为any类型,在函数体中直接读取参数message的length属性不会产生编译错误,因为编译器不会对any类型进行任何类型检查。但如果像第7行那样在调用f1函数时传入undefined值作为实际参数,则会产生运行时的类型错误。

在函数f2中,我们将参数message的类型定义为unknown类型。这样做的话,在函数体中就不能直接读取参数message的length属性,否则将产生编译错误。在使用unknown类型的参数message时,编译器会强制我们将其细化为某种具体类型。示例如下:

ts 复制代码
function f2(message: unknown) {
    if (typeof message === 'string') {
        return message.length;
    }
}

f2(undefined);

此例中,我们使用typeof运算符去检查参数message是否为字符串,只有当message是一个字符串时,我们才会去读取其length属性。这样修改之后,既不会产生编译错误,也不会产生运行时错误。

8、尾端类型

在类型系统中,尾端类型(Bottom Type)是所有其他类型的子类型。由于一个值不可能同时属于所有类型,例如一个值不可能同时为数字类型和字符串类型,因此尾端类型中不包含任何值。尾端类型也称作0类型或者空类型。

TypeScript中只存在一种尾端类型,即never类型。

8.1、never

TypeScript 2.0版本引入了仅有的尾端类型---never类型。never类型使用never关键字来标识,不包含任何可能值。示例如下:

ts 复制代码
function f(): never {
    throw new Error();
}

根据尾端类型的定义,never类型是所有其他类型的子类型。所以,never类型允许赋值给任何类型,尽管并不存在never类型的值。示例如下:

ts 复制代码
let x: never;

let a: boolean   = x;
let b: string    = x;
let c: number    = x;
let d: bigint    = x;
let e: symbol    = x;
let f: void      = x;
let g: undefined = x;
let h: null      = x;

正如尾端类型其名,它在类型系统中位于类型结构的最底层,没有类型是never类型的子类型。因此,除never类型自身外,所有其他类型都不能够赋值给never类型。示例如下:

ts 复制代码
let x: never;
let y: never;

// 正确
x = y;

// 错误
x = true;
x = 'hi';
x = 3.14;
x = 99999n;
x = Symbol();
x = undefined;
x = null;
x = {};
x = [];
x = function () {};

需要注意的是,就算是类型约束最宽松的any类型也不能够赋值给never类型。示例如下:

ts 复制代码
let x: any;
let y: never = x;
//  ~
//  编译错误:类型'any'不能赋值给类型'never'

8.2、应用场景

场景一:

never类型可以作为函数的返回值类型,它表示该函数无法返回一个值。我们知道,如果函数体中没有使用return语句,那么在正常执行完函数代码后会返回一个undefined值。在这种情况下,函数的返回值类型是void类型而不是never类型。只有在函数根本无法返回一个值的时候,函数的返回值类型才是never类型。

一种情况就是函数中抛出了异常,这会导致函数终止执行,从而不会返回任何值。在这种情况下,函数的返回值类型为never类型。示例如下:

ts 复制代码
function throwError(): never {
    throw new Error();

    // <- 该函数永远无法执行到末尾,返回值类型为'never'
}

此例中,throwError函数的功能是直接抛出一个异常,它永远也不会返回一个值,因此该函数的返回值类型为never类型。若函数中的代码不是直接抛出异常而是间接地抛出异常,那么函数的返回值类型也是never类型。示例如下:

ts 复制代码
function throwError(): never {
    throw new Error();
}

function fail(): never {
    return throwError();
}

此例中,fail函数包含了一条return语句,return语句中表达式的类型为never类型,因此fail函数的返回值类型也为never类型。

除了抛出异常之外,还有一种情况函数也无法正常返回一个值,即如果函数体中存在无限循环从而导致函数的执行永远也不会结束,那么在这种情况下函数的返回值类型也为never类型。示例如下:

ts 复制代码
function infiniteLoop(): never {
    while (true) {
        console.log('endless...');
    }
}

此例中,infiniteLoop函数的执行永远也不会结束,这意味着它无法正常返回一个值。因此,该函数的返回值类型为never类型。

场景二

在"条件类型"中常使用never类型来帮助完成一些类型运算。例如,​"Exclude<T, U>"类型是TypeScript内置的工具类型之一,它借助于never类型实现了从类型T中过滤掉类型U的功能。示例如下:

ts 复制代码
type Exclude<T, U> = T extends U ? never : T; 

下例中,我们使用"Exclude<T, U>"工具类型从联合类型"boolean |string"中剔除了string类型,最终得到的结果类型为boolean类型。示例如下:

ts 复制代码
type T = Exclude<boolean | string, string>; // boolean

场景三

最后一个要介绍的never类型的应用场景与类型推断功能相关。在TypeScript编译器执行类型推断操作时,如果发现已经没有可用的类型,那么推断结果为never类型。示例如下:

ts 复制代码
function getLength(message: string) {
    if (typeof message === 'string') {
        message; // string
    } else {
        message; // never
    }
}

此例中,getLength函数声明定义了参数message的类型为string。第2行,在if语句中使用typeof运算符来判断message是否为string类型。若参数message为string类型,则执行该分支内的代码。因此,第3行中参数message的类型为string类型。

第5行,在else分支中参数message的类型应该是非string类型。由于函数声明中定义了参数message的类型是string类型,因此else分支中已经不存在其他可选类型。在这种情况下,TypeScript编译器会将参数message的类型推断为never类型,表示不存在这样的值。

9、数组类型

数组是十分常用的数据结构,它表示一组有序元素的集合。在TypeScript中,数组值的数据类型为数组类型。

9.1、数组类型定义

TypeScript提供了以下两种方式来定义数组类型:

  • 简便数组类型表示法。
  • 泛型数组类型表示法。

以上两种数组类型定义方式仅在编码风格上有所区别,两者在功能上没有任何差别。

简便数组类型表示法:

简便数组类型表示法借用了数组字面量的语法,通过在数组元素类型之后添加一对方括号"​[​]​"来定义数组类型。它的语法如下所示:

ts 复制代码
TELement[]

该语法中,TElement代表数组元素的类型,方括号"​[​]​"代表数组类型。在TElement与"​[​]​"之间不允许出现换行符号。

下例中,我们使用"number[​]​"类型注解定义了常量digits的类型为number数组类型,它表示digits数组中元素的类型为number类型。示例如下:

ts 复制代码
01 const digits: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

如果数组中元素的类型为复合类型,则需要在数组元素类型上使用分组运算符,即小括号。例如,下例中的red数组既包含字符串元素也包含数字元素。因此,red数组元素的类型为string类型和number类型构成的联合类型,即"string | number"​。在使用简便数组类型表示法时,必须先将联合类型放在分组运算符内,然后再在后面添加一对方括号。示例如下:

ts 复制代码
const red: (string | number)[] = ['f', f, 0, 0, 0, 0];

此例中,若在类型注解里没有使用分组运算符,则表示string类型和number[​]类型的联合类型,即"string | (number[​])"​。该类型与实际数组类型不兼容,因此将产生编译错误。示例如下:

ts 复制代码
const red: string | number[] = ['f', 'f', 0, 0, 0, 0];
//    ~~~
//    编译错误

泛型数组类型表示法:

泛型数组类型表示法是另一种表示数组类型的方法。顾名思义,泛型数组类型表示法就是使用泛型来表示数组类型。它的语法如下所示:

ts 复制代码
Array<TElement>

该语法中,Array代表数组类型;​""是类型参数的语法,其中TElement代表数组元素的类型。

下例中,我们使用"Array"类型注解定义了常量digits的类型为number数组类型,它表示digits数组中元素的类型为number类型。示例如下:

ts 复制代码
const digits: Array<number> = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

在使用泛型数组类型表示法时,就算数组中元素的类型为复合类型也不需要使用分组运算符。我们还是以既包含字符串元素也包含数字元素的red数组为例,示例如下:

ts 复制代码
const red: Array<string | number> = ['f', 'f', 0, 0, 0, 0];

此例中,我们不再需要对联合类型"string | number"使用分组运算符。

两种方法比较:

正如前文所讲,简便数组类型表示法和泛型数组类型表示法在功能上没有任何差别,两者只是在编程风格上有所差别。

在定义简单数组类型时,如数组元素为单一原始类型或类型引用,使用简便数组类型表示法更加清晰和简洁。示例如下:

ts 复制代码
let a: string[];

let b: HTMLButtonElement[];

如果数组元素是复杂类型,如对象类型和联合类型等,则可以选择使用泛型数组类型表示法。它也许能让代码看起来更加整洁一些。示例如下:

ts 复制代码
let a: Array<string | number>;
 
let b: Array<{ x: number; y: number }>;

总结起来,目前存在以下三种常见的编码风格供读者参考:

  • 始终使用简便数组类型表示法。
  • 始终使用泛型数组类型表示法。
  • 当数组元素类型为单一原始类型或类型引用时,始终使用简便数组类型表示法;在其他情况下不做限制。

9.2、数组元素类型

在定义了数组类型之后,当访问数组元素时能够获得正确的元素类型信息。示例如下:

ts 复制代码
const digits: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

const zero = digits[0];
//    ~~~~
//    number类型

此例中,虽然没有给常量zero添加类型注解,但是TypeScript编译器能够从数组类型中推断出zero的类型为number类型。

我们知道,当访问数组中不存在的元素时将返回undefined值。TypeScript的类型系统无法推断出是否存在数组访问越界的情况,因此即使访问了不存在的数组元素,还是会得到声明的数组元素类型。示例如下:

ts 复制代码
const digits: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// 没有编译错误
const out: number = digits[100];

9.3、只读数组

只读数组与常规数组的区别在于,只读数组仅允许程序读取数组元素而不允许修改数组元素。

TypeScript提供了以下三种方式来定义一个只读数组:

  • 使用"ReadonlyArray<T>"内置类型。
  • 使用readonly修饰符。
  • 使用"Readonly<T>"工具类型。

以上三种定义只读数组的方式只是语法不同,它们在功能上没有任何差别。

ReadonlyArray<T>

在TypeScript早期版本中,提供了"ReadonlyArray"类型专门用于定义只读数组。在该类型中,类型参数T表示数组元素的类型。示例如下:

ts 复制代码
const red: ReadonlyArray<number> = [255, 0, 0];

readonly

TypeScript 3.4版本中引入了一种新语法,使用readonly修饰符能够定义只读数组。在定义只读数组时,将readonly修饰符置于数组类型之前即可。示例如下:

ts 复制代码
const red: readonly number[] = [255, 0, 0];

注意,readonly修饰符不允许与泛型数组类型表示法一起使用。示例如下:

ts 复制代码
const red: readonly Array<number> = [255, 0, 0];
//         ~~~~~~~~
//         编译错误

Readonly<T>

"Readonly<T>"是TypeScript提供的一个内置工具类型,用于定义只读对象类型。该工具类型能够将类型参数T的所有属性转换为只读属性,它的定义如下所示:

ts 复制代码
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

由于TypeScript 3.4支持了使用readonly修饰符来定义只读数组,所以从TypeScript 3.4开始可以使用"Readonly"工具类型来定义只读数组。示例如下:

ts 复制代码
const red: Readonly<number[]> = [255, 0, 0];

需要注意的是,类型参数T的值为数组类型"number[​]​"​,而不是数组元素类型number。在这一点上,它与"ReadonlyArray<T>"类型是有区别的。

注意事项:

我们可以通过数组元素索引来访问只读数组元素,但是不能修改只读数组元素。示例如下:

ts 复制代码
const red: readonly number[] = [255, 0, 0];

red[0];         // 正确

red[0] = 0;     // 编译错误

在只读数组上也不支持任何能够修改数组元素的方法,如push和pop方法等。示例如下:

ts 复制代码
const red: readonly number[] = [255, 0, 0];

red.push(0);     // 编译错误
red.pop();       // 编译错误

在进行赋值操作时,允许将常规数组类型赋值给只读数组类型,但是不允许将只读数组类型赋值给常规数组类型。换句话说,不能通过赋值操作来放宽对只读数组的约束。示例如下:

ts 复制代码
const a: number[] = [0];
const ra: readonly number[] = [0];

const x: readonly number[] = a;  // 正确

const y: number[] = ra;          // 编译错误
相关推荐
cui_win2 小时前
Rocky Linux(9.7) 完整使用指南(ISO下载 + 系统安装 + 网络配置)
linux·网络·rocky linux
有毒的教程2 小时前
Ubuntu 日常常用命令速查表(新手版)
linux·服务器·ubuntu
鬼才血脉2 小时前
CentOS 7 安装 MySQL 8.0
linux·mysql·centos
曹牧2 小时前
Java:解析Json字符串格式要求
java·linux·运维·前端
qzhqbb2 小时前
Linux 挖矿病毒自救方案
linux·运维·chrome
黑蛋同志3 小时前
Ubuntu安装llama.cpp
linux·ubuntu·llama
yy_xzz3 小时前
【Linux开发】 Linux 信号处理——预防僵尸进程
linux·运维·信号处理
火星机器人life3 小时前
turtlebot3 Ubuntu 20.04 + ROS2 Foxy+Gazebo 11.15.1 环境下编译运行
数据库·ubuntu·mfc
xianjian09123 小时前
Java进阶-在Ubuntu上部署SpringBoot应用
java·spring boot·ubuntu