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
。
unknown
跟any
的相似之处,在于所有类型的值都可以分配给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 有两个“顶层类型”(any
和unknown
),但是“底层类型”只有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种类型之中,undefined
和null
其实是两个特殊值,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
类型。
事实上,除了undefined
和null
这两个值不能转为对象,其他任何值都可以赋值给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的特殊性
undefined
和null
既是值,又是类型。
作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefined
或null
。
let age:number = 24; age = null; // 正确 age = undefined; // 正确
上面代码中,变量age
的类型是number
,但是赋值为null
或undefined
并不报错。
这并不是因为undefined
和null
包含在number
类型里面,而是故意这样设计,任何类型的变量都可以赋值为undefined
和null
,以便跟 JavaScript 的行为保持一致。
JavaScript 的行为是,变量如果等于undefined
就表示还没有赋值,如果等于null
就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。
但是有时候,这并不是开发者想要的行为,也不利于发挥类型系统的优势。
const obj:object = undefined; obj.toString() // 编译不报错,运行就报错
上面示例中,变量obj
等于undefined
,编译不会报错。但是,实际执行时,调用obj.toString()
就报错了,因为undefined
不是对象,没有这个方法。
为了避免这种情况,及早发现错误,TypeScript 提供了一个编译选项strictNullChecks
。只要打开这个选项,undefined
和null
就不能赋值给其他类型的变量(除了any
类型和unknown
类型)。
这个选项在配置文件tsconfig.json
的写法如下。
{ "compilerOptions": { "strictNullChecks": true // ... } }
打开strictNullChecks
以后,undefined
和null
这两种值也不能互相赋值了。
// 打开 strictNullChecks let x:undefined = null; // 报错 let y:null = undefined; // 报错
上面示例中,undefined
类型的变量赋值为null
,或者null
类型的变量赋值为undefined
,都会报错。
总之,打开strictNullChecks
以后,undefined
和null
只能赋值给自身,或者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';
上面示例中,变量x
是const
命令声明的,TypeScript 就会推断它的类型是值https
,而不是string
类型。
这样推断是合理的,因为const
命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。
注意,const
命令声明的变量,如果赋值为对象,并不会推断为值类型。
// x 的类型是 { foo: number } const x = { foo: 1 };
上面示例中,变量x
没有被推断为值类型,而是推断属性foo
的类型是number
。这是因为 JavaScript 里面,const
变量赋值为对象时,属性值是可以改变的。
7.联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|
表示。
联合类型A|B
表示,任何一个类型只要属于A
或B
,就属于联合类型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
表示,任何一个类型必须同时属于A
和B
,才属于交叉类型A&B
,即交叉类型同时满足A
和B
的特征。
交叉类型的主要用途是表示对象的合成。
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[]
理解成number
和string[]
的联合类型。
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
上面示例中,变量x
和y
的类型都是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 // 报错
而且,由于变量a
和b
是两个类型,也不能把一个赋值给另一个。
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 类型允许返回undefined
或null
。
如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用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 };
上面示例中,对象属性名的类型分别为number
和symbol
。
对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 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); }
上面示例就不会报错,因为函数体内部只使用了属性x
和y
,这两个属性有明确的类型声明,保证obj.x
和obj.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;
上面示例中,各种类型的值(除了null
和undefined
)都可以赋值给空对象类型,跟Object
类型的行为是一样的。
interface 接口
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。
interface 可以表示对象的各种语法,它的成员有5种形式:
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
(1)对象属性
interface Point { x: number; y: number; }
上面示例中,x
和y
都是对象的属性,分别使用冒号指定每个属性的类型。
(2)对象的属性索引
interface A { [prop: string]: number; }
上面示例中,[prop: string]
就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
属性索引共有string
、number
和symbol
三种类型。
(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
其实有两个属性name
和radius
。这时,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
就具有属性x
、y()
和z
。
接口合并
多个同名接口会合并成一个接口。
interface Box { height: number; width: number; } interface Box { length: number; }
上面示例中,两个Box
接口会合并成一个接口,同时有height
、width
和length
三个属性。
为什么会有接口合并这样一个功能呢?
举例来说,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
类同时实现了MotorVehicle
、Flyable
、Swimmable
三个接口。这意味着,它必须部署这三个接口声明的所有属性和方法,满足它们的所有条件。
但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代:
- 第一种方法是类的继承。
- 第二种方法是接口的继承。
注意,一个接口同时实现多个接口时,不同接口不能有互相冲突的属性。
类与接口的合并
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)控制:public
、private
和protected
。
这三个修饰符的位置,都写在属性或方法的最前面。
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
修饰符,构造方法的参数名只要有private
、protected
、readonly
修饰符,都可以添加。
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;
上面示例中,类A
的getName()
添加了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 内部数据结构,比如Map
、Set
和Promise
,其实也是泛型接口,完整的写法是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
的约束条件是U
、U
的约束条件是T
),因为互相约束就意味着约束条件就是类型参数自身。
Enum类型
实际开发中,经常需要定义一组相关的常量, 就是 Enum 结构,用来将相关常量放在一个容器里面,方便使用。
enum Color { Red, // 0 Green, // 1 Blue // 2 }
上面示例声明了一个 Enum 结构Color
,里面包含三个成员Red
、Green
和Blue
。第一个成员的值默认为整数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
是类型断言,它们必须满足下面的条件:expr
是T
的子类型,或者T
是expr
的子类型。
也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
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
断言后,类型推断就变成了只读元组。
非空断言
对于那些可能为空的变量(即可能等于undefined
或null
),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
,其中asserts
和is
都是关键词,value
是函数的参数名,string
是函数参数的预期类型。它的意思是,该函数用来断言参数value
的类型是string
,如果达不到要求,程序就会在这里中断。
使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。
另外,断言函数的asserts
语句等同于void
类型,所以如果返回除了undefined
和null
以外的值,都会报错。