大家好,我是有用就扩散,有用就点赞。
TypeScript 基本用法
类型声明
1)类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。函数参数和返回值,也是这样来声明类型。
2)TypeScript 规定,变量只有赋值后才能使用,否则就会报错。
类型推断
1)类型声明并不是必需的,如果没有,TypeScript 会自己推断类型。后面,如果变量更改为其他类型的值,跟推断的类型不一致,TypeScript 就会报错。
TypeScript 的编译
1)JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。所以,TypeScript 项目要想运行,必须先转为 JavaScript 代码,这个代码转换的过程就叫做“编译”(compile)。
2)TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。
3)因此,TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。
值与类型
1)TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。
2)这一点务必牢记。TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。前者使用 JavaScript 语法,后者使用 TypeScript 的类型语法。
3)它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。
4)编写 TypeScript 项目时,不要混淆哪些是值代码,哪些是类型代码。
TypeScript Playground
1)最简单的 TypeScript 使用方法,就是使用官网的在线编译页面,叫做 TypeScript Playground。
tsc 编译器
TypeScript 官方提供的编译器叫做 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本。
1)安装:$ npm install -g typescript
2)检查一下是否安装成功:$ tsc -v
3)帮助信息:$ tsc -h
4)完整的帮助信息:$ tsc --all
5)编译脚本
(1)
tsc
命令后面,加上 TypeScript 脚本文件,就可以将其编译成 JavaScript 脚本:$ tsc app.ts(2)
tsc
命令也可以一次编译多个 TypeScript 脚本。(3)如果想将多个 TypeScript 脚本编译成一个 JavaScript 文件,使用
--outFile
参数。(4)编译结果默认都保存在当前目录,
--outDir
参数可以指定保存到其他目录。(5)可以使用
--target
参数,指定编译后的 JavaScript 版本。6)编译错误的处理
(1)如果编译报错,
tsc
命令就会显示报错信息,但是这种情况下,依然会编译生成 JavaScript 脚本。(2)如果希望一旦报错就停止编译,不生成编译产物,可以使用
--noEmitOnError
参数(3)tsc 还有一个
--noEmit
参数,只检查类型是否正确,不生成 JavaScript 文件。7)tsconfig.json
(1)TypeScript 允许将
tsc
的编译参数,写在配置文件tsconfig.json
。只要当前目录有这个文件,tsc
就会自动读取,所以运行时可以不写参数。
ts-node 模块
ts-node 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码。
any类型
1)any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
2)设为
any
,TypeScript 实际上会关闭这个变量的类型检查。3)any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。
4)对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是
any
。
unknown类型
1)与
any
含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any
那样自由,可以视为严格版的any
。2)
unknown
类型的变量,不能直接赋值给其他类型的变量(除了any
类型和unknown
类型)。3)
unknown
类型的变量a
经过typeof
运算以后,能够确定实际类型是number
,就能用于加法运算了。这就是“类型缩小”,即将一个不确定的类型缩小为更明确的类型。4)
目的
是,只有明确unknown
变量的实际类型,才允许使用它,防止像any
那样可以随意乱用,“污染”其他变量。5)总之,
unknown
可以看作是更安全的any
。一般来说,凡是需要设为any
类型的地方,通常都应该优先考虑设为unknown
类型。
never
1)该类型为空,不包含任何值。
2)如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于
never
类型。3)不可能返回值的函数,返回值的类型就可以写成
never
4)
never
类型的一个重要特点是,可以赋值给任意其他类型。5)为什么
never
类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。
总之,TypeScript 有两个“顶层类型”(any
和unknown
),但是“底层类型”只有never
唯一一个。
数组类型
1)所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。
2)数组的类型有两种写法。第一种写法是在数组成员的类型后面,加上一对方括号。第二种写法是使用 TypeScript 内置的 Array 接口。另外,数组类型还有第三种写法,因为很少用到,使用interface 泛型接口。
3)正是由于成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错。
4)TypeScript 允许使用方括号读取数组成员的类型。由于数组成员的索引类型都是
number
,所以Array[number]读取表示数组所有数值索引的成员类型
数组的类型推断
1)如果数组变量没有声明类型,TypeScript 就会推断数组成员的类型。
2)如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是
any[]
。后面,为这个数组赋值时,TypeScript 会自动更新类型推断。如果初始值不是空数组,类型推断就不会更新。
只读数组,const 断言
1)JavaScript 规定,
const
命令声明的数组变量是可以改变成员的。2)TypeScript 允许声明只读数组,方法是在数组类型前面加上
readonly
关键字。TypeScript 将readonly number[]
与number[]
视为两种不一样的类型,后者是前者的子类型。注意,readonly
关键字不能与数组的泛型写法一起使用。实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。泛型ReadonlyArray<T>
和Readonly<T[]>
都可以用来生成只读数组类型。3)只读数组还有一种声明方法,就是使用“const 断言”。
多维数组
TypeScript 使用
T[][]
的形式,表示二维数组,T
是最底层数组成员的类型。
class 类型
属性的类型
1)类的属性可以在顶层声明,也可以在构造方法内部声明。
2)TypeScript 有一个配置项
strictPropertyInitialization
,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。
readonly 修饰符
1)属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。
2)readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。
3)构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。
方法的类型
1)类的方法就是普通函数,类型声明方式与函数一致。
2)构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
存取器方法
1)存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。
2)TypeScript 对存取器有以下规则。
(1)如果某个属性只有
get
方法,没有set
方法,那么该属性自动成为只读属性。(2)TypeScript 5.1 版之前,
set
方法的参数类型,必须兼容get
方法的返回值类型,否则报错。TypeScript 5.1 版做出了改变,现在两者可以不兼容。(3)
get
方法与set
方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
属性索引
1)类允许定义属性索引。
2)注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
3)属性存取器视同属性。
类的 interface 接口
1)implements 关键字
(1)interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
(2)注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
2)实现多个接口
(1)类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
3)类与接口的合并
(1)TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
Class 类型
1)实例类型
(1)TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
(2)作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。
(3)由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。
2)类的自身类型
(1)要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。
(2)类的自身类型就是一个构造函数,可以单独定义一个接口来表示
3)结构类型原则
(1)Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
(2)如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型。
类的继承
1)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
2)子类可以覆盖基类的同名方法。子类的同名方法不能与基类的类型定义相冲突。
3)如果基类包括保护成员(
protected
修饰符),子类可以将该成员的可访问性设置为公开(public
修饰符),也可以保持保护成员不变,但是不能改用私有成员(private
修饰符)4)注意,
extends
关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了。
可访问性修饰符
1)
public
修饰符表示这是公开成员,外部可以自由访问。public
修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。正常情况下,除非为了醒目和代码可读性,public
都是省略不写的。2)
private
修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。注意,子类不能定义父类私有成员的同名成员。如果在类的内部,当前类的实例可以获取私有成员。(1)严格地说,
private
定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private
关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private
成员没有严格禁止,使用方括号写法([]
)或者in
运算符,实例对象就能访问该成员。(2)由于
private
存在这些问题,加上它是 ES2022 标准发布前出台的,而 ES2022 引入了自己的私有成员写法#propName
。因此建议不使用private
,改用 ES2022 的写法,获得真正意义的私有成员。(3)构造方法也可以是私有的,这就直接防止了使用
new
命令生成实例对象,只能在类的内部创建实例对象。这时一般会有一个静态方法,充当工厂函数,强制所有实例都通过该方法生成。3)
protected
修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。(1)子类不仅可以拿到父类的保护成员,还可以定义同名成员。
(2)在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
4)实例属性的简写形式
(1)构造方法的参数
x
前面有public
修饰符,这时 TypeScript 就会自动声明一个公开属性x
,不必在构造方法里面写任何代码,同时还会设置x
的值为构造方法的参数值。注意,这里的public
不能省略。(2)除了
public
修饰符,构造方法的参数名只要有private
、protected
、readonly
修饰符,都会自动声明对应修饰符的实例属性。(3)
readonly
还可以与其他三个可访问性修饰符,一起使用。
顶层属性的处理方法
1)写法一是直接声明一个实例属性
age
,并初始化;写法二是顶层属性的简写形式,直接将构造方法的参数currentYear
声明为实例属性。2)先进行顶层属性的初始化,再运行构造方法。这在某些情况下,会使得同一段代码在 TypeScript 和 JavaScript 下运行结果不一致。
3)这种不一致一般发生在两种情况。
(1)第一种情况是,顶层属性的初始化依赖于其他实例属性。
(2)第二种情况与类的继承有关,子类声明的顶层属性在父类完成初始化。
4)为了解决这个问题,同时保证以前代码的行为一致,TypeScript 从3.7版开始,引入了编译设置
useDefineForClassFields
。这个设置设为true
,则采用 ES2022 标准的处理方法,否则采用 TypeScript 早期的处理方法。5)如果希望避免这种不一致,让代码在不同设置下的行为都一样,那么可以将所有顶层属性的初始化,都放到构造方法里面。
6)另一种解决方法,就是使用
declare
命令,去声明子类顶层属性的类型,告诉 TypeScript 这些属性的初始化由父类实现。
静态成员
1)静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
2)
static
关键字前面可以使用 public、private、protected 修饰符。3)静态私有属性也可以用 ES6 语法的
#
前缀表示。4)
public
和protected
的静态成员可以被继承。
泛型类
1)类也可以写成泛型,使用类型参数。
2)注意,静态成员不能使用泛型的类型参数。
抽象类,抽象成员
1)TypeScript 允许在类的定义前面,加上关键字
abstract
,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。2)抽象类只能当作基类使用,用来在它的基础上定义子类。
3)抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
4)抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有
abstract
关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。5)这里有几个注意点。
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加
abstract
关键字。(3)抽象成员前也不能有
private
修饰符,否则无法在子类中实现该成员。(4)一个子类最多只能继承一个抽象类。
总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
类this 问题
1)类的方法经常用到
this
关键字,它表示该方法当前所在的对象。2)有些场合需要给出
this
类型,但是 JavaScript 函数通常不带有this
参数,这时 TypeScript 允许函数增加一个名为this
的参数,放在参数列表的第一位,用来描述函数内部的this
关键字的类型。3)
this
参数的类型可以声明为各种对象。4)TypeScript 提供了一个
noImplicitThis
编译选项。如果打开了这个设置项,如果this
的值推断为any
类型,就会报错。5)在类的内部,
this
本身也可以当作类型使用,表示当前类的实例对象。注意,this
类型不允许应用于静态成员。
类型断言
1)类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。
2)类型断言有两种语法。两种语法是等价的,
value
表示值,Type
表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。(1)语法一:<类型>值(示例:value)
(2)语法二:值 as 类型(示例:value as Type)
3)类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。
4)类型断言的一大用处是,指定 unknown 类型的变量的具体类型。
类型断言的条件
1)类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
2)如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。
as const 断言
1)如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。
2)const 命令有更强的限定作用,可以缩小变量的类型范围。
3)TypeScript 提供了一种特殊的类型断言
as const
,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。4)使用了
as const
断言以后,let 变量就不能再改变值了。as const
断言只能用于字面量,不能用于变量。另外,as const
也不能用于表达式。5)
as const
会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。6)
as const
会将数组变成只读元组,所以很适合用于函数的 rest 参数。7)Enum 成员也可以使用
as const
断言。如果使用as const
断言了被Enum某个成员赋值的变量,变量的类型就被推断为 Enum 的某个成员。
非空断言
1)TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号
!
。2)非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。
3)非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。可以使用非空断言,表示这个属性肯定会有值。
4)另外,非空断言只有在打开编译选项
strictNullChecks
时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined
或null
。
断言函数
1)断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
2)为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。
asserts value is type
,其中asserts
和is
都是关键词,value
是函数的参数名,type
是函数参数的预期类型。3)如果要断言参数非空,可以使用工具类型
NonNullable<T>
。4)注意,断言函数与类型保护函数(type guard)是两种不同的函数。它们的区别是,断言函数不返回值,而类型保护函数总是返回一个布尔值。
5)如果要断言某个参数保证为真(即不等于
false
、undefined
和null
),TypeScript 提供了断言函数的一种简写形式。asserts x
省略了谓语和宾语,表示参数x
保证为真(true
)。
interface 接口
1)interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。
2)interface 可以表示对象的各种语法,它的成员有5种形式。
(1)对象属性
(2)对象的属性索引:属性索引共有
string
、number
和symbol
三种类型。一个接口中最多只能定义一个数值索引。(3)对象的方法:对象的方法共有三种写法。
(4)函数:可以用来声明独立的函数。
(5)构造函数:可以使用
new
关键字,表示构造函数。
interface 的继承
1)interface 可以使用
extends
关键字,继承其他 interface。interface 允许多重继承。多重接口继承,实际上相当于多个父接口的合并。如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。2)interface 可以继承
type
命令定义的对象类型。注意,如果type
命令定义的类型不是对象,interface 就无法继承。3)interface 还可以继承 class,即继承该类的所有成员。
接口合并
1)多个同名接口会合并成一个接口。这样的设计主要是为了兼容 JavaScript 的行为。
2)同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。
3)同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
4)如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。
interface 与 type 的异同
1)
interface
命令与type
命令作用类似,都可以表示对象类型。2)interface 与 type 的区别有下面几点。
(1)
type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。(2)
interface
可以继承其他类型,type
不支持继承。(3)同名
interface
会自动合并,同名type
则会报错。(4)
interface
不能包含属性映射(mapping),type
可以。(5)
this
关键字只能用于interface
。(6)type 可以扩展原始数据类型,interface 不行。
(7)
interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。3)继承的主要作用是添加属性,
type
定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。4)继承时,type 和 interface 是可以换用的。interface 可以继承 type。type 也可以继承 interface。
欢迎各位大哥投稿PR。