EXIT

00:00:00

TypeScript

[toc]


基础用法

1.类型声明

TypeScript 规定,变量只有赋值后才能使用,否则就会报错。

let x:number; console.log(x) // 报错

上面示例中,变量x没有赋值就被读取,导致报错。而 JavaScript 允许这种行为,不会报错,没有赋值的变量会返回undefined

2.类型推断

类型声明并不是必需的,如果没有,TypeScript 会自己推断类型。

let foo = 123;

上面示例中,变量foo并没有类型声明,TypeScript 就会推断它的类型。由于它被赋值为一个数值,因此 TypeScript 推断它的类型为number

后面,如果变量foo更改为其他类型的值,跟推断的类型不一致,TypeScript 就会报错。

因为TypeScript的类型推断,所以函数返回值的类型通常是省略不写的。

3.ts-node

ts-node 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码。

如果只是想简单运行 TypeScript 代码看看结果,ts-node 不失为一个便捷的方法。

any类型 unknow类型 never类型

1.any类型

any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。

对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any

​ TypeScript 提供了一个编译选项noImplicitAny,打开该选项,只要推断出any类型就会报错。

污染问题

any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。

let x:any = 'hello'; let y:number; y = x; // 不报错 y * 123 // 不报错 y.toFixed() // 编译不报错

上面示例中,变量x的类型是any,实际的值是一个字符串。变量y的类型是number,表示这是一个数值变量,但是它被赋值为x,这时并不会报错。然后,变量y继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。

2.unknow类型

为了解决any类型“污染”其他变量的问题,TypeScript 3.0 引入了unknown类型。它与any含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any那样自由,可以视为严格版的any

unknownany的相似之处,在于所有类型的值都可以分配给unknown类型。

unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制:

  • 首先,unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)

  • 其次,不能直接调用unknown类型变量的方法和属性。

  • 再次,unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符=====!=!==||&&?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。

那么,怎么才能使用unknown类型变量呢?

答案是只有经过“类型缩小”,unknown类型变量才可以使用。所谓“类型缩小”,就是缩小unknown变量的类型范围,确保不会出错。

let a:unknown = 1; if (typeof a === 'number') { let r = a + 10; // 正确 }

上面示例中,unknown类型的变量a经过typeof运算以后,能够确定实际类型是number,就能用于加法运算了。这就是“类型缩小”,即将一个不确定的类型缩小为更明确的类型。

这样设计的目的是,只有明确unknown变量的实际类型,才允许使用它,防止像any那样可以随意乱用,“污染”其他变量。类型缩小以后再使用,就不会报错。

总之,unknown可以看作是更安全的any。一般来说,凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。

在集合论上,unknown也可以视为所有其他类型(除了any)的全集,所以它和any一样,也属于 TypeScript 的顶层类型。

3.Never类型

不存在任何属于“空类型”的值,该类型被称为never,即不可能有这样的值。

never类型的一个重要特点是,可以赋值给任意其他类型。

为什么never类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never类型。因此,never类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。

总之,TypeScript 有两个“顶层类型”(anyunknown),但是“底层类型”只有never唯一一个。

类型系统

1.基本类型

JavaScript 语言(注意,不是 TypeScript)将值分成8种类型。

  • boolean 略
  • string 略
  • number 略
  • bigint 略
  • symbol 略
  • object
  • undefined 略
  • null 略

TypeScript 继承了 JavaScript 的类型设计,以上8种类型可以看作 TypeScript 的基本类型。

object:

根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数。

const x:object = { foo: 123 }; const y:object = [1, 2, 3]; const z:object = (n:number) => n + 1;

上面示例中,对象、数组、函数都属于 object 类型。

2.包装对象类型

JavaScript 的8种类型之中,undefinednull其实是两个特殊值,object属于复合类型,剩下的五种属于原始类型,代表最基本的、不可再分的值。这五种原始类型的值,都有对应的包装对象。所谓“包装对象”,指的是这些值在需要时,会自动产生的对象。

在 JavaScript 语言中,只有对象才有方法,原始类型的值本身没有方法。

'hello'.charAt(1) // 'e'

这行代码之所以可以运行,就是因为在调用方法时,字符串会自动转为包装对象,charAt()方法其实是定义在包装对象上。

五种包装对象之中,symbol 类型和 bigint 类型无法直接获取它们的包装对象(即Symbol()BigInt()不能作为构造函数使用),但是剩下三种可以。

Boolean() String() Number()

以上三个构造函数,执行后可以直接获取某个原始类型值的包装对象。

const s = new String('hello'); typeof s // 'object' s.charAt(1) // 'e'

上面示例中,s就是字符串hello的包装对象,typeof运算符返回object,不是string,但是本质上它还是字符串,可以使用所有的字符串方法。

注意,String()只有当作构造函数使用时(即带有new命令调用),才会返回包装对象。如果当作普通函数使用(不带有new命令),返回就是一个普通字符串。其他两个构造函数Number()Boolean()也是如此。

3.包装对象类型与字面量类型

由于包装对象的存在,导致每一个原始类型的值都有包装对象和字面量两种情况。

'hello' // 字面量 new String('hello') // 包装对象

为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。

  • Boolean 和 boolean
  • String 和 string
  • Number 和 number
  • BigInt 和 bigint
  • Symbol 和 symbol

其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象

建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。

4.大写Object类型和小写object类型

大写的Object

大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。原始类型值、对象、数组、函数都是合法的Object类型。

事实上,除了undefinednull这两个值不能转为对象,其他任何值都可以赋值给Object类型。

另外,空对象{}Object类型的简写形式,所以使用Object时常常用空对象代替。

let obj:{};

上面示例中,变量obj的类型是空对象{},就代表Object类型。

显然,无所不包的Object类型既不符合直觉,也不方便使用。

小写的Object

小写的object类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。

注意,无论是大写的Object类型,还是小写的object类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。

const o1:Object = { foo: 0 }; const o2:object = { foo: 0 }; o1.toString() // 正确 o1.foo // 报错 o2.toString() // 正确 o2.foo // 报错

上面示例中,toString()是对象的原生方法,可以正确访问。foo是自定义属性,访问就会报错。

5.undefined和null的特殊性

undefinednull既是值,又是类型。

作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefinednull

let age:number = 24; age = null; // 正确 age = undefined; // 正确

上面代码中,变量age的类型是number,但是赋值为nullundefined并不报错。

这并不是因为undefinednull包含在number类型里面,而是故意这样设计,任何类型的变量都可以赋值为undefinednull,以便跟 JavaScript 的行为保持一致。

JavaScript 的行为是,变量如果等于undefined就表示还没有赋值,如果等于null就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。

但是有时候,这并不是开发者想要的行为,也不利于发挥类型系统的优势。

const obj:object = undefined; obj.toString() // 编译不报错,运行就报错

上面示例中,变量obj等于undefined,编译不会报错。但是,实际执行时,调用obj.toString()就报错了,因为undefined不是对象,没有这个方法。

为了避免这种情况,及早发现错误,TypeScript 提供了一个编译选项strictNullChecks。只要打开这个选项,undefinednull就不能赋值给其他类型的变量(除了any类型和unknown类型)。

这个选项在配置文件tsconfig.json的写法如下。

{ "compilerOptions": { "strictNullChecks": true // ... } }

打开strictNullChecks以后,undefinednull这两种值也不能互相赋值了。

// 打开 strictNullChecks let x:undefined = null; // 报错 let y:null = undefined; // 报错

上面示例中,undefined类型的变量赋值为null,或者null类型的变量赋值为undefined,都会报错。

总之,打开strictNullChecks以后,undefinednull只能赋值给自身,或者any类型和unknown类型的变量。

6.值类型

TypeScript 规定,单个值也是一种类型,称为“值类型”。

let x:'hello'; x = 'hello'; // 正确 x = 'world'; // 报错

上面示例中,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。

TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

// x 的类型是 "https" const x = 'https'; // y 的类型是 string const y:string = 'https';

上面示例中,变量xconst命令声明的,TypeScript 就会推断它的类型是值https,而不是string类型。

这样推断是合理的,因为const命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。

注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。

// x 的类型是 { foo: number } const x = { foo: 1 };

上面示例中,变量x没有被推断为值类型,而是推断属性foo的类型是number。这是因为 JavaScript 里面,const变量赋值为对象时,属性值是可以改变的。

7.联合类型

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。

联合类型A|B表示,任何一个类型只要属于AB,就属于联合类型A|B

let x:string|number; x = 123; // 正确 x = 'abc'; // 正确

上面示例中,变量x就是联合类型string|number,表示它的值既可以是字符串,也可以是数值。

联合类型可以与值类型相结合,表示一个变量的值有若干种可能。

let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';

“类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。

8.交叉类型

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。

交叉类型A&B表示,任何一个类型必须同时属于AB,才属于交叉类型A&B,即交叉类型同时满足AB的特征。

交叉类型的主要用途是表示对象的合成

let obj: { foo: string } & { bar: string }; obj = { foo: 'hello', bar: 'world' };

9.type 命令

type命令用来定义一个类型的别名。

type Age = number; let age:Age = 55;

上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。

别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。

别名不允许重名。

别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。

别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。

type World = "world"; type Greeting = `hello ${World}`;

上面示例中,别名Greeting使用了模板字符串,读取另一个别名World

10.typeof运算符

JavaScript 里面,typeof运算符只可能返回八种结果,而且都是字符串。

TypeScript 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。

typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量foo的类型,这时使用typeof foo就可以获得它的类型。

11.块级类型声明

TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。

数组类型

JavaScript 数组在 TypeScript 里面分成两种类型,分别是数组(array)和元组(tuple)。本节进行数组的介绍.

TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。

数组的类型有两种写法:

​ 1. 第一种写法是在数组成员的类型后面,加上一对方括号。

let arr:number[] = [1, 2, 3];

​ 如果数组成员的类型比较复杂,可以写在圆括号里面。

let arr:(number|string)[];

这个例子里面的圆括号是必须的,否则因为竖杠|的优先级低于[],TypeScript 会把number|string[]理解成numberstring[]的联合类型。

​ 2. 数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。

let arr:Array<number> = [1, 2, 3];

​ 这种写法对于成员类型比较复杂的数组,代码可读性会稍微好一些。

let arr:Array<number|string>;

TypeScript 允许使用方括号读取数组成员的类型。

type Names = string[]; type Name = Names[0]; // string

上面示例中,类型Names是字符串数组,那么Names[0]返回的类型就是string

由于数组成员的索引类型都是number,所以读取成员类型也可以写成下面这样。

type Names = string[]; type Name = Names[number]; // string

上面示例中,Names[number]表示数组Names所有数值索引的成员类型,所以返回string

数组的类型推断

如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是any[]

后面,为这个数组赋值时,TypeScript 会自动更新类型推断。

const arr = []; arr // 推断为 any[] arr.push(123); arr // 推断类型为 number[] arr.push('abc'); arr // 推断类型为 (string|number)[]

但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。

只读数组

JavaScript 规定,const命令声明的数组变量是可以改变成员的。

TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly关键字。

const arr:readonly number[] = [0, 1];

TypeScript 将readonly number[]number[]视为两种不一样的类型,后者是前者的子类型。

这是因为只读数组没有pop()push()之类会改变原数组的方法,所以number[]的方法数量要多于readonly number[],这意味着number[]其实是readonly number[]的子类型。

我们知道,子类型继承了父类型的所有特征,并加上了自己的特征,所以子类型number[]可以用于所有使用父类型的场合,反过来就不行。

let a1:number[] = [0, 1]; let a2:readonly number[] = a1; // 正确 a1是子类型 a2是父类型 父类型可以等于子类型,因为子类型满足父类型所有特征 a1 = a2; // 报错:这是因为a2是父类型,他没有a1所具有的东西

父类型 = 子类型: 父类型有的,子类型都可以给.

子类型 != 父类型: 子类型有的,父类型无法给.

注意,readonly关键字不能与数组的泛型写法一起使用。

实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。

const a1:ReadonlyArray<number> = [0, 1]; const a2:Readonly<number[]> = [0, 1];

只读数组还有一种声明方法,就是使用“const 断言”。

const arr = [0, 1] as const;

as const告诉 TypeScript,推断类型时要把变量arr推断为只读数组,从而使得数组成员无法改变。

多维数组

TypeScript 使用T[][]的形式,表示二维数组,T是最底层数组成员的类型。

var multi:number[][] = [[1,2,3], [23,24,25]];

元组类型

元组(tuple)是 TypeScript 特有的数据类型,JavaScript 没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。由于成员的类型可以不一样,所以元组必须明确声明每个成员的类型。

const s:[string, string, boolean] = ['a', 'b', true];

元组类型的写法,与上一章的数组有一个重大差异。数组的成员类型写在方括号外面(number[]),元组的成员类型是写在方括号里面([number])。TypeScript 的区分方法就是,成员类型写在方括号里面的就是元组,写在外面的就是数组

使用元组时,必须明确给出类型声明(上例的[number]),不能省略,否则 TypeScript 会把一个值自动推断为数组。

// a 的类型被推断为 (number | boolean)[] let a = [1, true];

元组成员的类型可以添加问号后缀(?),表示该成员是可选的。

let a:[number, number?] = [1];

注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。

由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。

但是,使用扩展运算符(...),可以表示不限成员数量的元组。

type NamedNums = [ string, ...number[] ]; const a:NamedNums = ['A', 1, 2]; const b:NamedNums = ['B', 1, 2, 3];

扩展运算符(...)用在元组的任意位置都可以,它的后面只能是一个数组或元组。

type t1 = [string, number, ...boolean[]]; type t2 = [string, ...boolean[], number]; type t3 = [...boolean[], string, number];

元组可以通过方括号,读取成员类型。

type Tuple = [string, number]; type Age = Tuple[1]; // number

上面示例中,Tuple[1]返回1号位置的成员类型。

由于元组的成员都是数值索引,即索引类型都是number,所以可以像下面这样读取。

type Tuple = [string, number, Date]; type TupleEl = Tuple[number]; // string|number|Date

上面示例中,Tuple[number]表示元组Tuple的所有数值索引的成员类型,所以返回string|number|Date,即这个类型是三种值的联合类型。

只读元组

// 写法一 type t = readonly [number, string] // 写法二 type t = Readonly<[number, string]>

成员数量推断

如果没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量(即元组长度)。

如果包含了可选成员,TypeScript 会推断出可能的成员数量。

如果使用了扩展运算符,TypeScript 就无法推断出成员数量。

一旦扩展运算符使得元组的成员数量无法推断,TypeScript 内部就会把该元组当成数组处理。

Symbol类型

let x:symbol = Symbol(); let y:symbol = Symbol(); x === y // false

上面示例中,变量xy的类型都是symbol,且都用Symbol()生成,但是它们是不相等的。

Unique symbol

symbol类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值。

为了解决这个问题,TypeScript 设计了symbol的一个子类型unique symbol,它表示单个的、某个具体的 Symbol 值。

因为unique symbol表示单个值,所以这个类型的变量是不能修改值的,只能用const命令声明,不能用let声明

每个声明为unique symbol类型的变量,它们的值都是不一样的,其实属于两个值类型

const a:unique symbol = Symbol(); const b:unique symbol = Symbol(); a === b // 报错

而且,由于变量ab是两个类型,也不能把一个赋值给另一个。

unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行。

const a:unique symbol = Symbol(); //子类型 const b:symbol = a; // 正确 const c:unique symbol = b; // 报错

unique symbol 类型的一个作用,就是用作属性名(对象的属性),这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol

const x:unique symbol = Symbol(); const y:symbol = Symbol(); interface Foo { [x]: string; // 正确 [y]: string; // 报错 }

上面示例中,变量y当作属性名,但是y的类型是 symbol,不是固定不变的值,导致报错。

unique symbol类型也可以用作类(class)的属性值,但只能赋值给类的readonly static属性。

class C { static readonly foo:unique symbol = Symbol(); }

上面示例中,静态只读属性foo的类型就是unique symbol。注意,这时static和readonly两个限定符缺一不可,这是为了保证这个属性是固定不变的。

类型推断

如果变量声明时没有给出类型,TypeScript 会推断某个 Symbol 值变量的类型。

let命令声明的变量,推断类型为 symbol。

// 类型为 symbol let x = Symbol();

const命令声明的变量,推断类型为 unique symbol。

// 类型为 unique symbol const x = Symbol();

但是,const命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。

let x = Symbol(); // 类型为 symbol const y = x;

let命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。

const x = Symbol(); // 类型为 symbol let y = x;

函数类型

函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。

void 类型允许返回undefinednull

如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type命令为函数类型定义一个别名,便于指定给其他变量。

type MyFunc = (txt:string) => void; const hello:MyFunc = function (txt) { console.log('hello ' + txt); };

函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数。

一个很有用的技巧,任何需要类型的地方,都可以使用typeof运算符从一个获取类型。

可选参数

如果函数的某个参数可以省略,则在参数名后面加问号表示。

function f(x?:number) {...}

参数名带有问号,表示该参数的类型实际上是原始类型|undefined,它有可能为undefined。比如,上例的x虽然类型声明为number,但是实际上是number|undefined

函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。

可选参数与默认值不能同时使用。

rest参数

rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。

// rest 参数为数组 function joinNumbers(...nums:number[]) { // ... } // rest 参数为元组 function f(...args:[boolean, number]) { // ... }

注意,元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数。

function f( ...args: [boolean, string?] ) {}

void类型

函数返回类型为void的f,是一个没有返回值的函数。但是实际上,f的值可以是一个有返回值的函数(返回123),编译时不会报错。这是因为,这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。如果后面使用了这个函数的返回值,就违反了约定,则会报错。

注意,这种情况仅限于变量、对象方法和函数参数,函数字面量如果声明了返回值是 void 类型,还是不能有返回值。

never类型

never类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。

它主要有以下两种情况。

(1)抛出错误的函数。

function fail(msg:string):never { throw new Error(msg); }

上面示例中,函数fail()会抛出错误,不会正常退出,所以返回值类型是never

注意,只有抛出错误,才是 never 类型。如果显式用return语句返回一个 Error 对象,返回值就不是 never 类型。

(2)无限执行的函数。

const sing = function():never { while (true) { console.log('sing'); } };

上面示例中,函数sing()会永远执行,不会返回,所以返回值类型是never

注意,never类型不同于void类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回undefined

如果程序中调用了一个返回值类型为never的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。

一个函数如果某些条件下有正常返回值,另一些条件下抛出错误,这时它的返回值类型可以省略never

原因是前面章节提到过,never是 TypeScript 的唯一一个底层类型,所有其他类型都包括了never。从集合论的角度看,number|never等同于number。这也提示我们,函数的返回值无论是什么类型,都可能包含了抛出错误的情况。

函数重载

有一些编程语言允许不同的函数参数,对应不同的函数实现。但是,JavaScript 函数只能有一个实现,必须在这个实现当中,处理不同的参数。因此,函数体内部就需要判断参数的类型及个数,并根据判断结果执行不同的操作。

function add( x:number, y:number ):number; function add( x:any[], y:any[] ):any[]; function add( x:number|any[], y:number|any[] ):number|any[] { if (typeof x === 'number' && typeof y === 'number') { return x + y; } else if (Array.isArray(x) && Array.isArray(y)) { return [...x, ...y]; } throw new Error('wrong parameters'); }

上面示例中,前两行类型声明列举了重载的各种情况。第三行是函数本身的类型声明,它必须与前面已有的重载声明兼容。

对象类型

基本

对象的方法使用函数类型描述:

const obj:{ x: number; y: number; add(x:number, y:number): number; // 或者写成 // add: (x:number, y:number) => number; } = { x: 1, y: 1, add(x, y) { return x + y; } };

注意,如果属性值是一个对象,readonly修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。

另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量。

属性名的索引类型

如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。

索引类型里面,最常见的就是属性名的字符串索引。

type MyObj = { [property: string]: string }; const obj:MyObj = { foo: 'a', bar: 'b', baz: 'c', };

上面示例中,类型MyObj的属性名类型就采用了表达式形式,写在方括号里面。

[property: string]property表示属性名,这个是可以随便起的,它的类型是string,即属性名类型为string。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。

JavaScript 对象的属性名(即上例的property)的类型有三种可能,除了上例的string,还有number和**symbol**。

type T1 = { [property: number]: string }; type T2 = { [property: symbol]: string };

上面示例中,对象属性名的类型分别为numbersymbol

对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名

type MyType = { [x: number]: boolean; // 报错 [x: string]: string; }

同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突,否则报错。

type MyType = { foo: boolean; // 报错 [x: string]: string; }

上述代码发生错误是因为foo作为一个具体的属性,他的类型是boolean类型的,但是后面的索引类型[x: string]: string;中也包含了foo作为字符串的类型.本着必须服从后者的特性,foo是会报错的.

属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。

另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及length属性,因为类型里面没有定义这些东西。==> 声明的是伪数组.

解构赋值

解构赋值的类型写法,跟为对象声明类型是一样的。

const {id, name, price}:{ id: string; name: string; price: number } = product;

注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途:用作别名.

结构类型原则

只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。

type A = { x: number; }; type B = { x: number; y: number; };

上面示例中,对象A只有一个属性x,类型为number。对象B满足这个特征,因此兼容对象A,只要可以使用A的地方,就可以使用B

这种设计有时会导致令人惊讶的结果。

type myObj = { x: number, y: number, }; function getSum(obj:myObj) { let sum = 0; for (const n of Object.keys(obj)) { const v = obj[n]; // 报错 sum += Math.abs(v); } return sum; }

上面示例中,函数getSum()要求传入参数的类型是myObj,但是实际上所有与myObj兼容的对象都可以传入。

例如:下列代码中的myObj2就是myObj的子类型,他就兼容myObj.可以向getSum里面传入.

type myObj2 = { x: number, y: number, z: string, };

这会导致const v = obj[n]这一行报错,原因是obj[n]取出的属性值不一定是数值(number),使得变量v的类型被推断为any。如果项目设置为不允许变量类型推断为any,代码就会报错。写成下面这样,就不会报错。

type MyObj = { x: number, y: number, }; function getSum(obj:MyObj) { return Math.abs(obj.x) + Math.abs(obj.y); }

上面示例就不会报错,因为函数体内部只使用了属性xy,这两个属性有明确的类型声明,保证obj.xobj.y肯定是数值。虽然与MyObj兼容的任何对象都可以传入函数getSum(),但是只要不使用其他属性,就不会有类型报错。

严格字面类型检查

如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。

const point:{ x:number; y:number; } = { x: 1, y: 1, z: 1 // 报错 };

如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的

const myPoint = { x: 1, y: 1, z: 1 }; const point:{ x:number; y:number; } = myPoint; // 正确

TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。

最小可选属性规则

根据“结构类型”原则,如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的。

为了避免这种情况,TypeScript 2.4 引入了一个“最小可选属性规则”.

type Options = { a?:number; b?:number; c?:number; }; const opts = { d: 123 }; const obj:Options = opts; // 报错

上面示例中,对象opts与类型Options没有共同属性,赋值给该类型的变量就会报错。

报错原因是,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。

空对象

空对象作为类型,其实是Object类型的简写形式。

let d:{}; // 等同于 // let d:Object; d = {}; d = { x: 1 }; d = 'hello'; d = 2;

上面示例中,各种类型的值(除了nullundefined)都可以赋值给空对象类型,跟Object类型的行为是一样的。

interface 接口

interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。

interface 可以表示对象的各种语法,它的成员有5种形式:

  • 对象属性
  • 对象的属性索引
  • 对象方法
  • 函数
  • 构造函数

(1)对象属性

interface Point { x: number; y: number; }

上面示例中,xy都是对象的属性,分别使用冒号指定每个属性的类型。

(2)对象的属性索引

interface A { [prop: string]: number; }

上面示例中,[prop: string]就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。

属性索引共有stringnumbersymbol三种类型。

(3)对象的方法

对象的方法共有三种写法。

// 写法一 interface A { functionName (x: boolean): string; } // 写法二 interface B { functionName: (x: boolean) => string; } // 写法三 interface C { functionName: { (x: boolean): string }; }

(4)函数

interface 也可以用来声明独立的函数。

interface Add { (x:number, y:number): number; } const myAdd:Add = (x,y) => x + y;

(5)构造函数

interface 内部可以使用new关键字,表示构造函数。

interface ErrorConstructor { new (message?: string): Error; }

interface 继承 interface

interface 可以使用extends关键字,继承其他 interface。

interface Shape { name: string; } interface Circle extends Shape { radius: number; }

上面示例中,Circle继承了Shape,所以Circle其实有两个属性nameradius。这时,Circle是子接口,Shape是父接口。

interface 允许多重继承。

如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。

多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。

interface 继承 type

interface 可以继承type命令定义的对象类型。

type Country = { name: string; capital: string; } interface CountryWithPop extends Country { population: number; }

上面示例中,CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性。

注意,如果type命令定义的类型不是对象,interface 就无法继承。

interface 继承 class

interface 还可以继承 class,即继承该类的所有成员。关于 class 的详细解释 参见下一章

class A { x:string = ''; y():boolean { return true; } } interface B extends A { z: number }

上面示例中,B继承了A,因此B就具有属性xy()z

接口合并

多个同名接口会合并成一个接口。

interface Box { height: number; width: number; } interface Box { length: number; }

上面示例中,两个Box接口会合并成一个接口,同时有heightwidthlength三个属性。

为什么会有接口合并这样一个功能呢?

举例来说,Web 网页开发经常会对windows对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。

interface Document { foo: string; } document.foo = 'hello';

上面示例中,接口Document增加了一个自定义属性foo,从而就可以在document对象上使用自定义属性。同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。

同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。

这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。

如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。

interface与type的异同

很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。

它们的相似之处,首先表现在都能为对象类型起名。

interface 与 type 的区别有下面几点:

(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。

(2)interface可以继承其他类型,type不支持继承。

继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符进行交叉合并.

(3)同名(这里指的是类型名不是属性名)interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。

这表明,interface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。

(4)interface不能包含属性映射(mapping),type可以. 详见《映射》一章

(5)this关键字只能用于interface

(6)interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。

综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。

class类

类(class)是面向对象编程的基本构件,封装了属性和方法,js中ES6实现了class类的写法,ts则是对js中class进行的全面升级.

类的属性可以在顶层声明,也可以在构造方法内部声明。

对于顶层声明的属性,可以在声明时同时给出类型。

class Point { x:number; y:number; }

如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。

readonly只读属性

属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。

构造方法可以修改和设置只读属性的值。如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。

方法的类型

类的方法就是普通函数,类型声明方式与函数一致。

另外,构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。

存取器方法

存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。

它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。

class C { _name = ''; get name() { return this._name; } set name(value) { this._name = value; } }

get name(): 是取值器,其中get是关键词,**name**是属性名。外部读取name属性时,实例对象会自动调用这个方法,该方法的返回值就是name属性的值。

set name():是存值器,其中set是关键词,**name**是属性名。外部写入name属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入。

TypeScript 对存取器有以下规则:

(1)如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。该属性则不能再被重新赋值.

(2)get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。

类的属性索引

class MyClass { [s:string]: boolean | ((s:string) => boolean); get(s:string) { return this[s] as boolean; } }

上面示例中,[s:string]表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。

注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的返回值类型。

就如同上述例子中,属性索引s的本身作为属性的类型是string,s的类型是boolean和方法.包含了后者的get方法,所以不会报错.

类的interface接口

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

interface Country { name:string; capital:string; } // 或者 type Country = { name:string; capital:string; } class MyCountry implements Country { name = ''; capital = ''; }

interface 只是指定检查条件,如果不满足这些条件就会报错(如果interface声明了的属性你没有,就会报错)。它并不能代替 class 自身的类型声明,而且,类可以定义接口没有声明的方法和属性。

也就是说,虽然你 implements 了一个接口,但是你仍然需要在你的类中做类型声明.

implements关键字后面,**不仅可以是接口,也可以是另一个类。**这时,后面的类将被当作接口。

另外,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。

实现多个接口

类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。

class Car implements MotorVehicle, Flyable, Swimmable { // ... }

上面示例中,Car类同时实现了MotorVehicleFlyableSwimmable三个接口。这意味着,它必须部署这三个接口声明的所有属性和方法,满足它们的所有条件。

但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代:

  • 第一种方法是类的继承。
  • 第二种方法是接口的继承。

注意,一个接口同时实现多个接口时,不同接口不能有互相冲突的属性。

类与接口的合并

TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口合并进

class A { x:number = 1; } interface A { y:number; } let a = new A(); a.y = 10; a.x // 1 a.y // 10

class本身也是一种类型 ==疑惑==

实例类型

TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。

只有该class的实例对象才能用这个class类型.

class Color { name:string; constructor(name:string) { this.name = name; } } const green:Color = new Color('green');
// 错误 function createPoint( PointClass:Point, x: number, y: number ) { return new PointClass(x, y); }

上面示例中,函数createPoint()的第一个参数PointClass,需要传入 Point 这个类,但是如果把参数的类型写成Point就会报错,因为Point描述的是实例类型,而不是 Class 的自身类型。

类的自身属性

要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。

代表我不是实例对象,只是一个简单的对象,但是我需要这个class类型.

function createPoint( PointClass:typeof Point, x:number, y:number ):Point { return new PointClass(x, y); }

总结一下,类的自身类型就是一个构造函数,可以单独定义一个接口来表示。

结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。

class Foo { id!:number; } function fn(arg:Foo) { // ... } const bar = { id: 10, amount: 100, }; fn(bar); // 正确

上面示例中,对象bar满足类Foo的实例结构,只是多了一个属性amount。所以,它可以当作参数,传入函数fn()

如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。

class Person { name: string; } class Customer { name: string; } // 正确 const cust:Customer = new Person();

如果写成这样:

class Person { name: string; age: number; } class Customer { name: string; } // 正确 const cust:Customer = new Person(); // 报错 const cust:Person = new Customer();

因为根据“结构类型原则(父子原则)”,只要Person类具有name属性,就满足Customer类型的实例结构,所以可以代替它。反过来就不行,如果Customer类多出一个属性,就会报错。

类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。

class A { greet() { console.log('Hello, world!'); } } class B extends A { } const b = new B(); b.greet() // "Hello, world!"

上面示例中,子类B继承了基类A,因此就拥有了greet()方法,不需要再次在类的内部定义这个方法了。

根据结构类型原则,子类也可以用于类型为基类的场合。

子类可以覆盖基类的同名方法。

但是,子类的同名方法不能与基类的类型定义相冲突。(即:子类方法中的参数个数,参数类型要与基类的相同.)

class A { greet() { console.log('Hello, world!'); } } class B extends A { // 报错 greet(name:string) { console.log(`Hello, ${name}`); } } //但是这样是可以的 class B extends A { greet(name?: string) { if (name === undefined) { super.greet(); } else { console.log(`Hello, ${name}`); } } }

注意,extends关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了。

可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected

这三个修饰符的位置,都写在属性或方法的最前面。

private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。注意,子类不能定义父类私有成员的同名成员。

严格地说,private定义的私有成员,并不是真正意义的私有成员。

一方面,编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。因此建议不使用private,改用 ES2022 的写法,获得真正意义的私有成员。==> ES2022 的私有成员写法#propName

protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。

子类不仅可以拿到父类的保护成员,还可以定义同名成员。

实例属性的简写形式

实际开发中,很多实例属性的值,是通过构造方法传入的。

class Point { x:number; y:number; constructor(x:number, y:number) { this.x = x; this.y = y; } }

这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。

class Point { constructor( public x:number, public y:number ) {} }

上面示例中,构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。注意,这里的public不能省略。

除了public修饰符,构造方法的参数名只要有privateprotectedreadonly修饰符,都可以添加。

readonly还可以与其他三个可访问性修饰符,一起使用。

静态成员

类的内部可以使用static关键字,定义静态成员。

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

class MyClass { static x = 0; static printX() { console.log(MyClass.x); } } MyClass.x // 0 MyClass.printX() // 0

static关键字前面可以使用 public、private、protected 修饰符。

抽象类 抽象成员

TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。

abstract class A { id = 1; } const a = new A(); // 报错

抽象类只能当作基类使用,用来在它的基础上定义子类。

abstract class A { id = 1; } class B extends A { amount = 100; } const b = new B(); b.id // 1 b.amount // 100

抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。

抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member):即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

如果抽象类的方法前面加上abstract,就表明子类必须给出该方法的实现。

另外:

(1)抽象成员只能存在于抽象类,不能存在于普通类。

(2)抽象成员不能有具体实现的代码。

(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。

(4)一个子类最多只能继承一个抽象类。

this问题

类中的很多地方都需要用到this,但是如果:

class A { name = 'A'; getName() { return this.name; } } const a = new A(); a.getName() // 'A' const b = { name: 'b', getName: a.getName }; b.getName() // 'b'

那么A中的getName()函数输出的就不是实例对象的值了.

typeScript允许函数增加一个名为this的参数,放在参数列表的第一位,用来检查函数内部的this关键字的类型。

class A { name = 'A'; getName(this: A) { return this.name; } } const a = new A(); const b = a.getName;

上面示例中,类AgetName()添加了this参数,如果直接调用这个方法,this的类型就会跟声明的类型不一致,从而报错。

this参数的类型可以声明为各种对象,不一定非得是上述中的A.

function foo( this: { name: string } ) { this.name = 'Jack'; this.name = 0; // 报错 } foo.call({ name: 123 }); // 报错

在类的内部this本身也可以当作类型使用,表示当前类的实例对象,但是this类型不允许应用于静态成员。

泛型

泛型主要用在四个场合:函数、接口、类和别名。尽量少用泛型。

函数的泛型写法

function关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。

function id<T>(arg:T):T { return arg; }

那么对于箭头函数的写法:==注意,当泛型类型只有一个的时候要加一个逗号","==

let myId = <T,>(id:T):T => { return id; }

接口的泛型写法

interface 也可以采用泛型的写法。

interface Box<Type> { contents: Type; } let box:Box<string>;

类的泛型接口

泛型类的类型参数写在类名后面。

class Pair<K, V> { key: K; value: V; }

类型别名的泛型写法

type Nullable<T> = T | undefined | null;

类型参数的默认值

类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。

function getFirst<T = string>( arr:T[] ):T { return arr[0]; }

但是,因为 TypeScript 会从实际参数推断出T的值,从而覆盖掉默认值,所以下面的代码不会报错。

getFirst([1, 2, 3]) // 正确

一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。

<T = boolean, U> // 错误 <T, U = boolean> // 正确

数组的泛型表示

数组类型有一种表示方法是Array<T>。这就是泛型的写法,Array是 TypeScript 原生的一个类型接口,T是它的类型参数。声明数组时,需要提供T的值。

let arr:Array<number> = [1, 2, 3];

事实上,在 TypeScript 内部,数组类型的另一种写法number[]string[],只是Array<number>Array<string>的简写形式。

其他的 TypeScript 内部数据结构,比如MapSetPromise,其实也是泛型接口,完整的写法是Map<K, V>Set<T>Promise<T>

TypeScript 默认还提供一个ReadonlyArray<T>接口,表示只读数组。

类型参数的约束条件

很多类型参数并不是无限制的,对于传入的类型存在约束条件。

TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。

类型参数的约束条件采用下面的形式:

<TypeParameter extends ConstraintType>

上面语法中,TypeParameter表示类型参数,extends是关键字,这是必须的,ConstraintType表示类型参数要满足的条件,即类型参数应该是ConstraintType的子类型。

类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。

type Fn<A extends string, B extends string = 'world'> = [A, B]; type Result = Fn<'hello'> // ["hello", "world"]
function comp<T extends { length: number }>( a: T, b: T ) {...}

上面示例中,T extends { length: number }就是约束条件,表示类型参数 T 必须满足{ length: number },否则就会报错。

<T extends T> // 报错 <T extends U, U extends T> // 报错

上面示例中,T的约束条件不能是T自身。同理,多个类型参数也不能互相约束(即T的约束条件是UU的约束条件是T),因为互相约束就意味着约束条件就是类型参数自身。

Enum类型

实际开发中,经常需要定义一组相关的常量, 就是 Enum 结构,用来将相关常量放在一个容器里面,方便使用。

enum Color { Red, // 0 Green, // 1 Blue // 2 }

上面示例声明了一个 Enum 结构Color,里面包含三个成员RedGreenBlue。第一个成员的值默认为整数0,第二个为1,第三个为2,以此类推。

使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。

let c = Color.Green; // 1 // 等同于 let c = Color['Green']; // 1

Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number

let c:Color = Color.Green; // 正确 let c:number = Color.Green; // 正确

Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。

// 编译前 enum Color { Red, // 0 Green, // 1 Blue // 2 } // 编译后 let Color = { Red: 0, Green: 1, Blue: 2 };

上面示例是 Enum 结构编译前后的对比。由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。

Enum 成员的值

Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2……

但是,也可以为 Enum 成员显式赋值。

  • 成员的值可以是任意数值,可以是小数,但不能是大整数(Bigint)
  • 成员的值甚至可以相同.
  • 如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
  • Enum 成员值都是只读的,不能重新赋值。为了让这一点更醒目,通常会在 enum 关键字前面加上const修饰,表示这是常量,不能再次赋值。
enum Color { Red = 90, Green = 0.5, Blue = 7n // 报错 }

同名 Enum 的合并

  • 多个同名的 Enum 结构会自动合并。
  • Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
  • 同名 Enum 合并时,不能有同名成员,否则报错。

字符串 Enum

Enum 成员的值除了设为数值,还可以设为字符串。也就是说,Enum 也可以用作一组相关字符串的集合。

注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。

Enum 成员可以是字符串和数值混合赋值。

除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。

keyof 运算符... 反向映射 ...

类型断言

对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。

type T = 'a'|'b'|'c'; let foo = 'a'; let bar:T = foo; // 报错

上面示例中,最后一行报错,原因是 TypeScript 推断变量foo的类型是string,而变量bar的类型是'a'|'b'|'c',前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。

TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。

type T = 'a'|'b'|'c'; let foo = 'a'; let bar:T = foo as T; // 正确

上面示例中,最后一行的foo as T表示告诉编译器,变量foo的类型断言为T,所以这一行不再需要类型推断了,编译器直接把foo的类型当作T,就不会报错了。

类型断言有两种语法。

// 语法一:<类型>值 <Type>value // 语法二:值 as 类型 value as Type

推荐使用语法二: let bar:T = foo as T;

类型断言的一大用处是,指定 unknown 类型的变量的具体类型。类型断言也适合指定联合类型的值的具体类型。

类型断言的条件

expr as T

上面代码中,expr是实际的值,T是类型断言,它们必须满足下面的条件:exprT的子类型,或者Texpr的子类型。

也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

as const 断言

TypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。

let s = 'JavaScript' as const; setLang(s); // 正确

上面示例中,变量s虽然是用 let 命令声明的,但是使用了as const断言以后,就等同于是用 const 命令声明的,变量s的类型会被推断为值类型JavaScript

使用了as const断言以后,let 变量就不能再改变值了。

注意as const断言只能用于字面量,不能用于变量。

let s = 'JavaScript'; setLang(s as const); // 报错

另外,as const也不能用于表达式。 ==> let s = ('Java' + 'Script') as const; // 报错

数组字面量使用as const断言后,类型推断就变成了只读元组。

非空断言

对于那些可能为空的变量(即可能等于undefinednull),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!

非空断言在实际编程中很有用,有时可以省去一些额外的判断。

断言函数

断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。

function isString(value:unknown):void { if (typeof value !== 'string') throw new Error('Not a string'); }

上面示例中,函数isString()就是一个断言函数,用来保证参数value是一个字符串,否则就会抛出错误,中断程序的执行。

下面是它的用法。

function toUpper(x: string|number) { isString(x); return x.toUpperCase(); }

传统的断言函数isString()的写法有一个缺点,它的参数类型是unknown,返回值类型是void(即没有返回值)。单单从这样的类型声明,很难看出isString()是一个断言函数。

为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。

function isString(value:unknown):asserts value is string { if (typeof value !== 'string') throw new Error('Not a string'); }

上面示例中,函数isString()的返回值类型写成asserts value is string,其中assertsis都是关键词,value是函数的参数名,string是函数参数的预期类型。它的意思是,该函数用来断言参数value的类型是string,如果达不到要求,程序就会在这里中断。

使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。

另外,断言函数的asserts语句等同于void类型,所以如果返回除了undefinednull以外的值,都会报错。

uid:q3U7if
VOIDIS.ME
  1. no-like
  2. message
  3. Bilibili
  4. Github
  5. RSS
  6. sun