TypeScript(一)
1.什么是TS
2.ts的优势
3.ts版本选择
没有选择最新版,而是按照教程一致选择tsc 5.1.6
可以使用ts-node 运行ts文件
4.数据类型
4.1 八种数据类型
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,就需要
14.ts内置类型
如Array,Date,Math,Dom和Bom等
15.配置文件–tsconfig.json中
1 | "noImplicitAny": true, |