TypeScript(一)
1.什么是TS
/%E4%BB%80%E4%B9%88%E6%98%AFts.png)
2.ts的优势
/ts%E7%9A%84%E4%BC%98%E5%8A%BF.png)
3.ts版本选择
没有选择最新版,而是按照教程一致选择tsc 5.1.6
/ts%E7%89%88%E6%9C%AC%E9%80%89%E6%8B%A9.png)
可以使用ts-node 运行ts文件
4.数据类型
4.1 八种数据类型
/js%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B.png)
4.2 any类型和unknown类型区别
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
1  | let x: any;  | 
变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
1  | let x: any = "hello";  | 
实际开发中,any类型主要适用以下两个场合。
(1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为any。
(2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。
总之,TypeScript 认为,只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。
unknown
unknown跟any的相似之处,在于所有类型的值都可以分配给unknown类型。
1  | let x: unknown;  | 
unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制。
首先,unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。
1  | let v: unknown = 123;  | 
上面示例中,变量v是unknown类型,赋值给any和unknown以外类型的变量都会报错,这就避免了污染问题,从而克服了any类型的一大缺点。
其次,不能直接调用unknown类型变量的方法和属性。
1  | let v1: unknown = { foo: 123 };  | 
上面示例中,直接调用unknown类型变量的属性和方法,或者直接当作函数执行,都会报错。
再次,unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、===、!=、!==、||、&&、?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。
1  | let a: unknown = 1;  | 
4.3 void和never的区别
void会返回值,但只返回underfined或null
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。
由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。
上面示例中,变量x的类型是never,就不可能赋给它任何值,否则都会报错。
never类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。另外,不可能返回值的函数,返回值的类型就可以写成never,详见《函数》一章。
如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。
1  | function fn(x: string | number) {  | 
never类型的一个重要特点是,可以赋值给任意其他类型。
1  | function f(): never {  | 
上面示例中,函数f()会抛错,所以返回值类型可以写成never,即不可能返回任何值。各种其他类型的变量都可以赋值为f()的运行结果(never类型)。
总之,TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。
4.4 null和underfined的区别
undefined 和 null 是两种独立类型,它们各自都只有一个值。
undefined 类型只包含一个值undefined,表示未定义(即还未给出定义,以后可能会有定义)。
1  | let x: undefined = undefined;  | 
上面示例中,变量x就属于 undefined 类型。两个undefined里面,第一个是类型,第二个是值。
null 类型也只包含一个值null,表示为空(即此处没有值)。
1  | const x: null = null;  | 
上面示例中,变量x就属于 null 类型。
注意,如果没有声明类型的变量,被赋值为undefined或null,它们的类型会被推断为any。
1  | let a = undefined; // any  | 
如果希望避免这种情况,则需要打开编译选项strictNullChecks。
1  | // 打开编译设置 strictNullChecks  | 
上面示例中,打开编译设置strictNullChecks以后,赋值为undefined的变量会被推断为undefined类型,赋值为null的变量会被推断为null类型。
5.数组和元组
1  | //数组的使用  | 
6.可访问性修饰符
类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:public、private和protected。
这三个修饰符的位置,都写在属性或方法的最前面。
public
public修饰符表示这是公开成员,外部可以自由访问。
1  | class Greeter {  | 
上面示例中,greet()方法前面的public修饰符,表示该方法可以在类的外部调用,即外部实例可以调用。
public修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。
正常情况下,除非为了醒目和代码可读性,public都是省略不写的。
private
private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
1  | class A {  | 
上面示例中,属性x前面有private修饰符,表示这是私有成员。因此,实例对象和子类使用该成员,都会报错。
注意,子类不能定义父类私有成员的同名成员。
1  | class A {  | 
上面示例中,A类有一个私有属性x,子类B就不能定义自己的属性x了。
如果在类的内部,当前类的实例可以获取私有成员。
1  | class A {  | 
上面示例中,在类A内部,A的实例对象可以获取私有成员x。
严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。
1  | class A {  | 
上面示例中,A类的属性x是私有属性,但是实例使用方括号,就可以读取这个属性,或者使用in运算符检查这个属性是否存在,都可以正确执行。
由于private存在这些问题,加上它是 ES6 标准发布前出台的,而 ES6 引入了自己的私有成员写法#propName。因此建议不使用private,改用 ES6 的写法,获得真正意义的私有成员。
1  | class A {  | 
上面示例中,采用了 ES6 的私有成员写法(属性名前加#),TypeScript 就正确识别了实例对象没有属性x,从而报错。
构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象,只能在类的内部创建实例对象。
这时一般会有一个静态方法,充当工厂函数,强制所有实例都通过该方法生成。
1  | class Singleton {  | 
上面示例使用私有构造方法,实现了单例模式。想要获得 Singleton 的实例,不能使用new命令,只能使用getInstance()方法。
protected
protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
1  | class A {  | 
上面示例中,类A的属性x是保护成员,直接从实例读取该属性(a.x)会报错,但是子类B内部可以读取该属性。
子类不仅可以拿到父类的保护成员,还可以定义同名成员。
1  | class A {  | 
上面示例中,子类B定义了父类A的同名成员x,并且父类的x是保护成员,子类将其改成了公开成员。B类的x属性前面没有修饰符,等同于修饰符是public,外界可以读取这个属性。
在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
1  | class A {  | 
上面示例中,属性x是类A的保护成员,在类的外部,实例对象a拿不到这个属性。但是,实例对象a传入类A的内部,就可以从a拿到x。
readonly 修饰符
属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。
1  | class A {  | 
上面示例中,id属性前面有 readonly 修饰符,实例对象修改这个属性就会报错。
readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。
1  | class A {  | 
上面示例中,构造方法内部设置只读属性的初值,这是可以的。
1  | class A {  | 
上面示例中,构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。
7.静态成员–static
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
1  | class MyClass {  | 
上面示例中,x是静态属性,printX()是静态方法。它们都必须通过MyClass获取,而不能通过实例对象调用。
static关键字前面可以使用 public、private、protected 修饰符。
1  | class MyClass {  | 
上面示例中,静态属性x前面有private修饰符,表示只能在MyClass内部使用,如果在外部调用这个属性就会报错。
静态私有属性也可以用 ES6 语法的#前缀表示,上面示例可以改写如下。
1  | class MyClass {  | 
public和protected的静态成员可以被继承。
1  | class A {  | 
上面示例中,类A的静态属性x和y都被B继承,公开成员x可以在B的外部获取,保护成员y只能在B的内部获取。
单例模式在ts的实现
1  | //单例模式  | 
8.抽象类
抽象类,抽象成员
TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abastract class)。
1  | abstract class A {  | 
上面示例中,直接新建抽象类的实例,会报错。
抽象类只能当作基类使用,用来在它的基础上定义子类。
1  | abstract class A {  | 
上面示例中,A是一个抽象类,B是A的子类,继承了A的所有成员,并且可以定义自己的成员和实例化。
抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
1  | abstract class A {  | 
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
1  | abstract class A {  | 
上面示例中,抽象类A定义了抽象属性foo,子类B必须实现这个属性,否则会报错。
下面是抽象方法的例子。
如果抽象类的属性前面加上abstract,就表明子类必须给出该方法的实现。
1  | abstract class A {  | 
这里有几个注意点。
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
(4)一个子类最多只能继承一个抽象类。
总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
6.接口
类的 interface 接口
implements 关键字
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
1  | interface Country {  | 
上面示例中,interface或type都可以定义一个对象类型。类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。
interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。
1  | interface A {  | 
上面示例中,类B实现了接口A,但是后者并不能代替B的类型声明。因此,B的get()方法的参数s的类型是any,而不是string。B类依然需要声明参数s的类型。
1  | class B implements A {  | 
下面是另一个例子。
1  | interface A {  | 
上面示例中,接口A有一个可选属性y,类B没有声明这个属性,所以可以通过类型检查。但是,如果给B的实例对象的属性y赋值,就会报错。所以,B类还是需要声明可选属性y。
1  | class B implements A {  | 
同理,类可以定义接口没有声明的方法和属性。
1  | interface Point {  | 
上面示例中,MyPoint类实现了Point接口,但是内部还定义了一个额外的属性z,这是允许的,表示除了满足接口给出的条件,类还有额外的条件。
implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
1  | class Car {  | 
上面示例中,implements后面是类Car,这时 TypeScript 就把Car视为一个接口,要求MyCar实现Car里面的每一个属性和方法,否则就会报错。所以,这时不能因为Car类已经实现过一次,而在MyCar类省略属性或方法。
注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
1  | interface Foo {  | 
上面示例中,接口Foo有一个私有属性,结果就报错了。
实现多个接口
类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
1  | class Car implements MotorVehicle, Flyable, Swimmable {  | 
上面示例中,Car类同时实现了MotorVehicle、Flyable、Swimmable三个接口。这意味着,它必须部署这三个接口声明的所有属性和方法,满足它们的所有条件。
但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。
第一种方法是类的继承。
1  | class Car implements MotorVehicle {}  | 
上面示例中,Car类实现了MotorVehicle,而SecretCar类继承了Car类,然后再实现Flyable和Swimmable两个接口,相当于SecretCar类同时实现了三个接口。
第二种方法是接口的继承。
1  | interface A {  | 
上面示例中,接口B继承了接口A,类只要实现接口B,就相当于实现A和B两个接口。
前一个例子可以用接口继承改写。
1  | interface MotorVehicle {  | 
上面示例中,接口SuperCar通过SuperCar接口,就间接实现了多个接口。
注意,发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。
1  | interface Flyable {  | 
上面示例中,属性foo在两个接口里面的类型不同,如果同时实现这两个接口,就会报错。
类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
1  | class A {  | 
上面示例中,类A与接口A同名,后者会被合并进前者的类型定义。
1  | interface IPerson{  | 
7.函数
1  | // 定义一个函数add,参数x,y,z,z可选,返回值为number类型  | 
8.类型推断、联合类型和类型断言和type guard
9.类型断言
对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。
1  | type T = "a" | "b" | "c";  | 
上面示例中,最后一行报错,原因是 TypeScript 推断变量foo的类型是string,而变量bar的类型是'a'|'b'|'c',前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。
回到上面的例子,解决方法就是进行类型断言,在赋值时断言变量foo的类型。
1  | type T = "a" | "b" | "c";  | 
上面示例中,最后一行的foo as T表示告诉编译器,变量foo的类型断言为T,所以这一行不再需要类型推断了,编译器直接把foo的类型当作T,就不会报错了。
总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。
类型断言有两种语法。
1  | // 语法一:<类型>值  | 
上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
1  | // 语法一  | 
上面示例是两种类型断言的语法,其中的语法一因为跟 JSX 语法冲突,使用时必须关闭 TypeScript 的 React 支持,否则会无法识别。由于这个原因,现在一般都使用语法二。
下面看一个例子。《对象》一章提到过,对象类型有严格字面量检查,如果存在额外的属性会报错。
1  | // 报错  | 
上面示例中,等号右侧是一个对象字面量,多出了属性y,导致报错。解决方法就是使用类型断言,可以用两种不同的断言。
1  | // 正确  | 
上面示例中,两种类型断言都是正确的。第一种断言将类型改成与等号左边一致,第二种断言使得等号右边的类型是左边类型的子类型,子类型可以赋值给父类型,同时因为存在类型断言,就没有严格字面量检查了,所以不报错。
下面是一个网页编程的实际例子。
1  | const username = document.getElementById("username");  | 
上面示例中,变量username的类型是HTMLElement | null,排除了null的情况以后,HTMLElement 类型是没有value属性的。如果username是一个输入框,那么就可以通过类型断言,将它的类型改成HTMLInputElement,就可以读取value属性。
注意,上例的类型断言的圆括号是必需的,否则username会被断言成HTMLInputElement.value,从而报错。
类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。
1  | const data: object = {  | 
上面示例中,变量data是一个对象,没有length属性。但是通过类型断言,可以将它的类型断言为数组,这样使用length属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。
类型断言的一大用处是,指定 unknown 类型的变量的具体类型。
1  | const value: unknown = "Hello World";  | 
上面示例中,unknown 类型的变量value不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。
另外,类型断言也适合指定联合类型的值的具体类型。
1  | const s1: number | string = "hello";  | 
上面示例中,变量s1是联合类型,可以断言其为联合类型里面的一种具体类型,再将其赋值给变量s2。
类型断言的条件
类型断言并不意味着,可以把某个值断言为任意类型。
1  | const n = 1;  | 
上面示例中,变量n是数值,无法把它断言成字符串,TypeScript 会报错。
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。
1  | expr as T;  | 
上面代码中,expr是实际的值,T是类型断言,它们必须满足下面的条件:expr是T的子类型,或者T是expr的子类型。
也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any类型和unknown类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。
1  | // 或者写成 <T><unknown>expr  | 
上面代码中,expr连续进行了两次类型断言,第一次断言为unknown类型,第二次断言为T类型。这样的话,expr就可以断言成任意类型T,而不报错。
下面是本小节开头那个例子的改写。
1  | const n = 1;  | 
上面示例中,通过两次类型断言,变量n的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。
as const 断言
如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。
1  | // 类型推断为基本类型 string  | 
上面示例中,变量s1的类型被推断为string,变量s2的类型推断为值类型JavaScript。后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围。
有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。
1  | let s = "JavaScript";  | 
上面示例中,最后一行报错,原因是函数setLang()的参数language类型是Lang,这是一个联合类型。但是,传入的字符串s的类型被推断为string,属于Lang的父类型。父类型不能替代子类型,导致报错。
一种解决方法就是把 let 命令改成 const 命令。
1  | const s = "JavaScript";  | 
这样的话,变量s的类型就是值类型JavaScript,它是联合类型Lang的子类型,传入函数setLang()就不会报错。
另一种解决方法是使用类型断言。TypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。
1  | let s = "JavaScript" as const;  | 
上面示例中,变量s虽然是用 let 命令声明的,但是使用了as const断言以后,就等同于是用 const 命令声明的,变量s的类型会被推断为值类型JavaScript。
使用了as const断言以后,let 变量就不能再改变值了。
1  | let s = "JavaScript" as const;  | 
上面示例中,let 命令声明的变量s,使用as const断言以后,就不能改变值了,否则报错。
注意,as const断言只能用于字面量,不能用于变量。
1  | let s = "JavaScript";  | 
上面示例中,as const断言用于变量s,就报错了。下面的写法可以更清晰地看出这一点。
1  | let s1 = "JavaScript";  | 
另外,as const也不能用于表达式。
1  | let s = ("Java" + "Script") as const; // 报错  | 
上面示例中,as const用于表达式,导致报错。
as const也可以写成前置的形式。
1  | // 后置形式  | 
as const断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。
1  | const v1 = {  | 
上面示例中,第二种写法是对属性x缩小类型,第三种写法是对整个对象缩小类型。
总之,as const会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。
下面是数组的例子。
1  | // a1 的类型推断为 number[]  | 
上面示例中,数组字面量使用as const断言后,类型推断就变成了只读元组。
由于as const会将数组变成只读元组,所以很适合用于函数的 rest 参数。
1  | function add(x: number, y: number) {  | 
上面示例中,变量nums的类型推断为number[],导致使用扩展运算符...传入函数add()会报错,因为add()只能接受两个参数,而...nums并不能保证参数的个数。
事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。
解决方法就是使用as const断言,将数组变成元组。
1  | const nums = [1, 2] as const;  | 
上面示例中,使用as const断言后,变量nums的类型会被推断为readonly [1, 2],使用扩展运算符展开后,正好符合函数add()的参数类型。
Enum 成员也可以使用as const断言。
1  | enum Foo {  | 
上面示例中,如果不使用as const断言,变量e1的类型被推断为整个 Enum 类型;使用了as const断言以后,变量e2的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。
非空断言
对于那些可能为空的变量(即可能等于undefined或null),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!。
1  | function f(x?: number | null) {  | 
上面示例中,函数f()的参数x的类型是number|null,即可能为空。如果为空,就不存在x.toFixed()方法,这样写会报错。但是,开发者可以确认,经过validateNumber()的前置检验,变量x肯定不会为空,这时就可以使用非空断言,为函数体内部的变量x加上后缀!,x!.toFixed()编译就不会报错了。
非空断言在实际编程中很有用,有时可以省去一些额外的判断。
1  | const root = document.getElementById("root");  | 
上面示例中,getElementById()有可能返回空值null,即变量root可能为空,这时对它调用addEventListener()方法就会报错,通不过编译。但是,开发者如果可以确认root元素肯定会在网页中存在,这时就可以使用非空断言。
1  | const root = document.getElementById("root")!;  | 
上面示例中,getElementById()方法加上后缀!,表示这个方法肯定返回非空结果。
不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。
1  | const root = document.getElementById("root");  | 
上面示例中,如果root为空会抛错,比非空断言更保险一点。
非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。
1  | class Point {  | 
上面示例中,属性x和y会报错,因为 TypeScript 认为它们没有初始化。
这时就可以使用非空断言,表示这两个属性肯定会有值,这样就不会报错了。
1  | class Point {  | 
另外,非空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined或null。
1  | //类型推断  | 
9.枚举
实际开发中,经常需要定义一组相关的常量。
1  | const RED = 1;  | 
上面示例中,常量RED、GREEN、BLUE是相关的,意为变量color的三个可能的取值。它们具体等于什么值其实并不重要,只要不相等就可以了。
TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。
1  | enum Color {  | 
上面示例声明了一个 Enum 结构Color,里面包含三个成员Red、Green和Blue。第一个成员的值默认为整数0,第二个为1,第二个为2,以此类推。
使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。
1  | let c = Color.Green; // 1  | 
Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number。
1  | let c: Color = Color.Green; // 正确  | 
上面示例中,变量c的类型写成Color或number都可以。但是,Color类型的语义更好。
Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。
1  | // 编译前  | 
上面示例是 Enum 结构编译前后的对比。
由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。
Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
1  | enum Operator {  | 
很大程度上,Enum 结构可以被对象的as const断言替代。
1  | enum Foo {  | 
上面示例中,对象Bar使用了as const断言,作用就是使得它的属性无法修改。这样的话,Foo和Bar的行为就很类似了,前者完全可以用后者替代,而且后者还是 JavaScript 的原生数据结构。
10.泛型
有些时候,函数返回值的类型与参数类型是相关的。
1  | function getFirst(arr) {  | 
上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。
这个函数的类型声明只能写成下面这样。
1  | function f(arr: any[]): any {  | 
上面的类型声明,就反映不出参数与返回值之间的类型关系。
为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。
1  | function getFirst<T>(arr: T[]): T {  | 
上面示例中,函数getFirst()的函数名后面尖括号的部分<T>,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。
上例的函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[],那么 T 的值就是number,因此返回值类型也是number。
函数调用时,需要提供类型参数。
1  | getFirst<number>([1, 2, 3]);  | 
上面示例中,调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是<number>。
不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。
1  | getFirst([1, 2, 3]);  | 
上面示例中,TypeScript 会从实际参数[1, 2, 3],推断出类型参数 T 的值为number。
有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。
1  | function comb<T>(arr1: T[], arr2: T[]): T[] {  | 
上面示例中,两个参数arr1、arr2和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。
1  | comb([1, 2], ["a", "b"]); // 报错  | 
上面示例会报错,TypeScript 认为两个参数不是同一个类型。但是,如果类型参数是一个联合类型,就不会报错。
1  | comb<number | string>([1, 2], ["a", "b"]); // 正确  | 
上面示例中,类型参数是一个联合类型,使得两个参数都符合类型参数,就不报错了。这种情况下,类型参数是不能省略不写的。
类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。
下面是多个类型参数的例子。
1  | function map<T, U>(arr: T[], f: (arg: T) => U): U[] {  | 
上面示例将数组的实例方法map()改写成全局函数,它有两个类型参数T和U。含义是,原始数组的类型为T[],对该数组的每个成员执行一个处理函数f,将类型T转成类型U,那么就会得到一个类型为U[]的数组。
总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。
泛型的写法
泛型主要用在四个场合:函数、接口、类和别名。
函数的泛型写法
上一节提到,function关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。
1  | function id<T>(arg: T): T {  | 
那么对于变量形式定义的函数,泛型有下面两种写法。
1  | // 写法一  | 
接口的泛型写法
interface 也可以采用泛型的写法。
1  | interface Box<Type> {  | 
上面示例中,使用泛型接口时,需要给出类型参数的值(本例是string)。
下面是另一个例子。
1  | interface Comparator<T> {  | 
上面示例中,先定义了一个泛型接口,然后将这个接口用于一个类。
泛型接口还有第二种写法。
1  | interface Fn {  | 
上面示例中,Fn的类型参数Type的具体类型,需要函数id在使用时提供。所以,最后一行的赋值语句不需要给出Type的具体类型。
此外,第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。
类的泛型写法
泛型类的类型参数写在类名后面。
1  | class Pair<K, V> {  | 
下面是继承泛型类的例子。
1  | class A<T> {  | 
上面示例中,类A有一个类型参数T,使用时必须给出T的类型,所以类B继承时要写成A<any>。
泛型也可以用在类表达式。
1  | const Container = class<T> {  | 
上面示例中,新建实例时,需要同时给出类型参数T和类参数data的值。
下面是另一个例子。
1  | class C<NumType> {  | 
上面示例中,先新建类C的实例foo,然后再定义示例的value属性和add()方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。
JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。
1  | type MyClass<T> = new (...args: any[]) => T;  | 
上面示例中,函数createInstance()的第一个参数AnyClass是构造函数(也可以是一个类),它的类型是MyClass<T>,这里的T是createInstance()的类型参数,在该函数调用时再指定具体类型。
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
1  | class C<T> {  | 
上面示例中,静态属性data引用了类型参数T,这是不可以的,因为类型参数只能用于实例属性和实例方法,所以报错了。
类型别名的泛型写法
type 命令定义的类型别名,也可以使用泛型。
1  | type Nullable<T> = T | undefined | null;  | 
上面示例中,Nullable<T>是一个泛型,只要传入一个类型,就可以得到这个类型与undefined和null的一个联合类型。
下面是另一个例子。
1  | type Container<T> = { value: T };  | 
下面是定义树形结构的例子。
1  | type Tree<T> = {  | 
上面示例中,类型别名Tree内部递归引用了Tree自身。
泛型类
类也可以写成泛型,使用类型参数。
1  | class Box<Type> {  | 
上面示例中,类Box有类型参数Type,因此属于泛型类。新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box<string>可以省略不写,因为可以从等号右边推断得到。
注意,静态成员不能使用泛型的类型参数。
1  | class Box<Type> {  | 
1  | //泛型基本用法  | 
11.泛型在类和接口中的使用
1  | class Queue<T>{  | 
12.类型别名、字面量和交叉类型
1  | //类型别名  | 
13.声明文件
声明文件里面没有任何的实际实现代码,只有类型声明,比如interface,class,function等
axios.d.ts中:
1  | interface IAxios{  | 
test.ts中:
1  | axios.get('url')  | 
我们做项目有两种情况,有不需要引入声明文件的包,如axios,只需要npm i axios 就可
另一种为需要引入声明文件,比如jquery,就需要
/%E5%A3%B0%E6%98%8E%E6%96%87%E4%BB%B6.png)
14.ts内置类型
/%E5%86%85%E7%BD%AE%E5%AF%B9%E8%B1%A1.png)
如Array,Date,Math,Dom和Bom等
15.配置文件–tsconfig.json中
1  | "noImplicitAny": true,  | 
/%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.png)


