超级详细的typescript入坑指南
前言
一:为什么要学习ts
我们先来看下面这个js代码:
function getUserName() {
if (Math.random() < 0.5) {
return "zhang san";
}
return 404;
}
let myname = getUsername();
mynema = myname
.split(" ")
.filter((it) => it)
.map((it) => it[0].touppercase() + it.subStr(1))
.join(" ");
代码的内容很简单,是一个将名字首字母大写的功能。但是我们不难看出这段代码有很明显的几处错误:
1. 使用了不存在的变量、函数或成员。在上图中,getUserName
方法在调用时写成了getUsername
,声明的变量myname
在使用时写成了mynema
,toUpperCase
方法写成了touppercase
,substr
方法写成了subStr
;
2. 类型错误,把一个不确定的类型当做一个确定的类型处理。 在上图中,getUserName
方法返回的数据,可能会string
类型,也可能为number
类型,然而在使用时,完全把getUserName
方法返回的数据当做string
来处理,就会造成报错;
此外,我们在开发过程中很容易使用null
或undefined
中的成员。 如下图中所示:
当然,我们不会直接书写这样的代码。但是这个错误是很常见的,因为我们开发过程中使用的对象,大多可能是调用另一个函数或者请求后台接口获取的,甚至有可能开发时数据不够完善的情况下,一直没有暴露出这个问题,直到项目上线之后才发现这种错误,这无疑增加了修改bug的时间,且降低了项目的代码质量。
js语言创建之初,仅仅是为了解决一些简单的业务场景,如图片文字的展示、幻灯片播放,js语言本身的特性(即弱类型、解释型)不能在开发过程中及时提示这些错误,需要编译完成之后在浏览器才能看到报错信息。
二:ts的特点
ts是什么
引用ts官网中的描述:“typeScript
是js
的 超集,是一个 可选的、 静态的类型系统。”下面解释一下这几个关键词的含义:
超集:如整数和正整数的关系,即ts
包含js
的所有功能,且比js多一些功能。
ts比js多出来的部分,就是类型系统,对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查。
可选的:类型系统可以用,也可以不用。
静态的:无论是浏览器环境还是node环境,无法直接识别ts代码,需要使用ts编译器tsc转换为es。静态的类型检查发生的时间在编译的时候,而非运行时。
ts意外的惊喜
原本使用ts的目的仅仅是为了使用类型检查减少一些js开发中的错误,但是发现有了类型检查之后,无形中增强了面向对象的开发。js也有类和对象,本身就支持面向对象开发,但是由于缺少类型检查,在面向对象开发过程中会遇到很多问题。一般在开发过程中,除了使用react进行开发外,一般很少使用类进行开发。一方面是因为多数的前端开发工作者不习惯面向对象的思维模式,此外js缺少类型检查,使得面向对象开发的诸多场景会出现问题。使用ts后,可以编写出完善的面向对象代码。
三:ts的使用
在了解ts语法之前,我们先搭建一个学习ts的开发环境。
安装typescript:npm i -g typescript
,建议使用全局安装,这样可以更方便的使用tsc。
安装好之后,创建一个index.ts
文件,写入下面的代码
let say:string = 'hello'
执行tsc index.ts
,就会生成一个index.js
文件,内容如下:
let say = 'hello'
此时原本的index.ts
文件就会出现如下报错:
造成错误的原因:默认情况下,ts会自动作出以下几种假设:
- 假设当前执行的环境是浏览器的dom环境;
- 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行;
- 编译的目标代码是es3。
解决上述几种问题的方法:
- 在使用tsc命令时,加上选项参数(不实用,不推荐);
- 使用ts的配置文件,更改编译选项。
如何添加ts配置文件:
- 直接在资源管理器中添加一个名为
tsconfig.json
的新文件。 - 使用命令
tsc --init
生成配置文件
使用命令生成的配置文件有很多属性,并且默认会在后面标注属性的含义和使用方式,但并不是所有的属性都需要用到,可以先把所有的属性删除。
{
"compilerOptions": { // 编译选项
"target": "es2016", // 编译目标使用的js标准,如:es3、es5、es2015,默认是es3。注意:es6及之后发行的版本,都使用年份。如:es2015、es2017
"module": "commonjs", // 编译目标使用的模块化标准,如commonjs、es6
"lib": ["es2016"] // ts编译的环境,默认有"dom"环境,即在浏览器环境下编译。
// 去掉"dom"环境后,浏览器自带的document等方法就不生效了,同时console等方法也不生效了,然而ts没有自带的node环境,此时应该引入一个@types/node插件。
// @types是一个ts官方的类型库,其中包含了很多对js代码的类型描述。
// 使用这个库是由于js在引用一些第三方库时(jquery等),这些第三方库没有使用ts,而类型检查又需要检查这些第三方库,就可以在@types里面找一下有没有对应的类型系统。
// 比如jQuery,没有类型检查,在ts项目中想要使用jQuery,就可以安装@types/jquery,进行类型定义。
// 使用 npm i -D @types/node
}
}
相关配置完成后,ts文件中就已经不报错了。
加了配置文件之后,使用tsc进行编译时,就不能加上文件名了,如果加了文件名,编译时会忽略掉配置文件。在编译时直接使用tsc进行编译就行。
新建一个src
文件夹,将index.ts
文件复制到src
文件夹下。执行tsc
命令,默认情况下,使用tsc
进行编译的是整个工程下所有的代码,即src
目录下和根目录下的ts文件都会被编译。但是我们并不需要执行整个工程的代码,而是只需要编译src
目录下的文件,只需在配置文件中加一个属性:include
(与compilerOptions
同级),表示要编译哪个文件夹下的代码。
{
"include": ["./src"]
}
如果只需要编译src
下的某一个文件,则使用files
(与compilerOptions
同级)进行配置
{
"files": ["./src/index.ts"]
}
但是此时编译的结果是和ts文件是放在同一个文件夹下的,如果想要配置编译后的js文件的位置,就使用outDir
(compilerOptions
下级)进行配置
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"lib": ["es2016"],
"outDir": "./dist"
}
}
使用第三方库可以减少操作:
ts-node
将ts在内存中完成编译,同时完成运行不会产生js文件
执行命令:ts-node src/index.ts
nodemon
监测文件变化,保存文件后会自动编译和运行
执行命令:nodemon --exec ts-node src/index.ts
此时nodemon
会监测整个项目,如果需要设置只监测ts文件
执行命令:nodemon -e ts --exec ts-node src/index.ts
如果只需要监测src下的文件
执行命令:nodemon --watch src -e ts --exec ts-node src/index.ts
将这个命令配置到package.json
的脚本中,后续就可以使用命令开启一个开发环境,再使用tsc
打包到disc
文件夹中。
{
"scripts": {
"serve": "nodemon --watch src -e ts --exec ts-node src/index.ts",
"build": "tsc"
},
"dependencies": {
"@types/node": "^18.15.3",
"nodemon": "^2.0.21",
"ts-node": "^10.9.1"
}
}
四:ts语法
ts类型约束哪些内容:变量、函数的参数、函数的返回值
约束方法: 在变量、函数的参数、函数的返回值后面加上 : + 类型
例如:
变量约束
let say:string = 'hello'
这样在给say赋值的时候,如果赋值为非string的值,就会报错
函数的参数和返回值
function sum(a: number, b: number): number {
return a + b
}
这样在调用函数的时候,传入了非number
的参数就会报错
或者将这个函数的返回值赋值给一个非number
的变量
函数的返回值,即使不手动书写,ts也能自动监测到
如何知道ts什么时候能自动检测什么时候监测不到呢
下面出现三个点的情况下,可以认为是一个警告,表示无法检测到类型,会默认标记为any
类型,ts不会对any
类型进行任何类型检查。
在js中,由于没有类型检查,一个函数甚至可以赋值为一个字符串,而这种错误在ts中很容易被发现,如果命名重复,需要修改某个变量或函数名,而这个变量或函数已经调用多次,可以点击F2,直接修改名字,这样在项目中所有调用这个变量或函数的地方都会更改为新的名字。即使写在别的文件中,只要使用了模块化,也可以修改。
点击F12可以直接转到定义
ts基础类型
任意类型any
声明为any
的变量可以赋值为任何类型,将不会对any
类型的数据进行类型检查,所以any
类型的数据可以赋值给任何一个类型
let a: any = 123
a = '123'
数字类型number
双精度64位浮点值,可以用来表示整数和分数。
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
let decLiteral: number = 6; // 十进制
let hexLiteral: number = 0xf00d; // 十六进制
字符串类型string
let a: string = '123'
布尔类型boolean
表示逻辑值:true和false
let a: boolean = true
数组类型
在声明数组时
let arr: number[] = [1, 2]
let arr: Array<number> = [1, 2]
元组
用来表示 **固定长度 **的数组并且每一项的类型都确定,对应的位置类型必须相同
let x: [string, number];
x = ['Runoob', 1]; // 运行正常
x = [1, 'Runoob']; // 报错
console.log(x[0]); // 输出 Runoob
对象object
表示对象类型,智能约束这是个对象类型,但是没法精准的约束对象里面的每一项,所以并不常用
let obj: object = {}
void
用于标识函数没有任何返回值
function sum (a: number, b: number) {
console.log(a + b)
}
null
表示对象缺失,null和undefined是其他除了never以外的类型的子类型,即undefined和null可以赋值给其他任何类型
let a: null = null
let b: string = a
undefined
用于初始化变量一个未定义的值
// let a: undefined = undefined
// let b: string = a
将undefined
、null
或never
赋值给其他类型的情况下,这些值在调用一些方法的时候还是会报错,可以在配置文件的compilerOptions
里面加上属性strictNullChecks: true
,表示更加严格的校验,这样undefined
和null
就不能赋值给其他类型了。
never
表示函数永远不会结束或永远不存在的变量,是任何类型(包括null
和undefined
)的子类型,例如函数抛出一个错误或进入无限循环
function sum (a: number, b: number): never {
while (true) {
console.log(a + b)
}
}
联合类型
let num: string | number | undefined
字面量
表示使用一个值进行约束,而不是一个类型
let a: 'A’' // 表示a永远只能赋值为A
let sex: '男' | '女' // 强力约束性别永远只能取男或女
在给数组规定类型时,一定要规定数组的每一位的类型,如果写为 let arr: []
表示声明一个数组永远只能为空数组
在给对象进行约束时,就可以使用字面量
let obj: {
name: string;
age: number;
sex: "男" | "女";
};
可选参数
可以在某些参数名后面加上问号,表示该参数可以不用传递
function sum (a: number, b: number, c?: number) {}
此时参数c的类型被识别为c: number | undefined
可选参数的位置必须在末尾
默认参数
function sum (a: number, b: number, c: number = 0) {}
此时参数c的类型被识别为c?: number
在给函数的参数规定类型时,参数可能为多种情况,而不同情况可能会造成错误,可以在函数实现之前,对函数调用的多种情况进行声明
ts扩展类型
类型别名
例如上述对象字面量中,要声明一个对象数组
type gender = '男' | '女'
type obj = {
name: string
age: number
sex: gender
}
let arr: obj[]
表示这个数组中的对象的内容必须是这个对象字面量的内容
枚举enum
约束某个变量的取值范围
enum gender {
man = '男',
woman = '女'
}
enum level {
level1 = 1,
level2,
level3
}
console.log(level.level2)
字面量结合联合类型,也能实现枚举的效果(例如上述案例中,性别的枚举),但是为什么还需要枚举呢?
- 使用字面量结合联合类型,需要重复书写代码(可以使用类型别名解决)
- 逻辑名称和真实的值会产生混淆,导致修改真实值的时候,产生大量修改(使用
enum
可以直接修改类型的真实值,逻辑名称需要修改时,可以点击F2进行重构) - 字面量类型不会进入编译结果,
enum
编译后,表现为对象
编译为
枚举的字段值,可以是字符串也可以是数字,字段值为数字的枚举叫做数字枚举,数字枚举的值会自动递增
如果第一个也不赋值为1,默认从0开始
接口interface
用于约束类、对象、函数的契约(标准)
- 接口定义对象
interface User {
name: string
age: number
}
let user: User = {
name: 'zhangsan',
age: 28
}
目前接口和类型别名在约束对象时的区别不大,最大的区别在于接口可以约束类。一般情况下约束对象建议使用interface。interface的内容不会出现在编译结果中。
- 接口约束函数 约束对象中的方法:
interface User {
name: string
age: number
sayHello: () => void
}
let user: User = {
name: 'zhangsan',
age: 28,
sayHello: () => {
console.log('hello')
}
}
或:
interface User {
name: string
age: number
sayHello(): void
}
let user: User = {
name: 'zhangsan',
age: 28,
sayHello: () => {
console.log('hello')
}
}
直接约束函数,以下方函数举例:
function sum(numbers: number[], callback: (num: number) => boolean) {
let s = 0
numbers.forEach(item => {
if (callback(item)) {
s += item
}
})
return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))
使用类型别名也能将函数的类型提取出来:
type Condition = (num: number) => boolean
function sum(numbers: number[], callback: Condition) {
let s = 0
numbers.forEach(item => {
if (callback(item)) {
s += item
}
})
return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))
使用interface约束:
interface Condition {
(num: number): boolean
}
function sum(numbers: number[], callback: Condition) {
let s = 0
numbers.forEach(item => {
if (callback(item)) {
s += item
}
})
return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))
接口可以继承,可以让一个接口拥有另一个接口的所有成员
也可以同时继承多个接口:
使用类型别名可以使用交叉类型“&”实现同样的效果
但是在接口中不能定义一个重复的类型,而类型别名是可以的重复定义的
如果两次定义的类型不一样,最终的类型为两个类型的合并类型,例如:“number & string”,即这个值拥有string和number的所有属性,但是这样做没法赋值,毫无意义。
readonly
修饰符:修饰的目标是只读的
interface User {
id: string
name: string
age: number
}
let user: User = {
id: '123',
name: 'zhangsan',
age: 28
}
user.id = '456'
上述方法中,假设我们并不希望对id属性进行重新赋值,但有时因为方法过于复杂,会造成误操作。此时我们可以在id的类型前面加上修饰符。只读修饰符不会进入编译结果。
数组设置只读修饰符,这个数组依然可以重新赋值。
或
上面的方法表示这个变量是一个只读的数组,即不能使用push、splice等修改数据的方法,也不能单独改变数组中的某个成员
放到对象中就不能重新赋值了
但是可以使用push、splice等方法修改数组里面的值
如果希望数组不能重新赋值的同时,不能修改里面的内容:
类型的兼容性
对象的兼容性:
ts在判断类型时,并不是严格的要求对象中的属性类型一一对应,只要当前需要使用的某几位符合要求就可以。例如调用后台的接口请求过来的用户信息,可能包含很多内容,但是前端在使用时并不需要那么多内容,那么只需要定义代码中需要使用的属性的类型就行。
interface UserInfo {
name: string
age: number
}
let userInfo = {
name: 'zhangsan',
age: 28,
sex: 'man'
}
let user: UserInfo = userInfo
但是不能直接赋值:
函数的兼容性:
interface Condition {
(num: number, index: number): boolean
}
function sum(numbers: number[], callback: Condition) {
let s = 0
numbers.forEach((item, index) => {
if (callback(item, index)) {
s += item
}
})
return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))
上面的代码中,Condition
需要接收两个参数,然而在调用时,只传了一个参数,但是并不会报错,需要使用第二个参数时,直接使用就可以
console.log(sum([1, 2, 3], (num, index) => index % 2 !== 0))
类型断言
let value: any
value = 'abc'
console.log((<string>value).length)
console.log((value as string).length)
(value as string[]).push('hi')
联合类型可以被断言为其中一个类型 父类型可以被断言为子类型 任何类型都可以被断言为any类型 any类型可以被断言为一个子类型
类class
上述代码直接报错,因为ts要求一个类中所有的属性及属性的类型必须是设定好的,所以类中的属性必须在属性列表中设定,而不是在构造函数中动态添加。属性列表不会出现在编译结果中。
规定好一个类中的所有属性后,也不能动态的添加属性了
如果想要避免忘记在构造函数中设置属性的值,就在tsconfig.json
文件中配置:strictPropertyInitialization: true
设置属性的默认值:
class User {
name: string
age: number
sex: '男' | '女'
constructor(name: string, age: number, sex: '男' | '女' = '男') {
this.name = name
this.age = age
this.sex = sex
}
}
const user = new User('zhangsan', 28)
也可以:
class User {
name: string
age: number
sex: '男' | '女' = '男'
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
const user = new User('zhangsan', 28)
user.sex = '女'
可选的属性:
class User {
name: string
age: number
sex: '男' | '女' = '男'
pId: string | undefined
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
const user = new User('zhangsan', 28)
user.pId = '123123'
也可以:
class User {
name: string
age: number
sex: '男' | '女' = '男'
pId?: string
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
const user = new User('zhangsan', 28)
user.pId = '123123'
只读的属性:
不能在外部使用的属性:
访问修饰符可以控制类中的某个成员的访问权限,不会出现在编译结果中
public
:默认的,公开的权限,所有的代码均可访问
private
:私有的,只有在类内部可以使用
protected
:受保护的
如果某个属性,是在构造函数中直接传进来,然后直接赋值的(例如上述案例中的name
和age
),可以简写为:
class User {
sex: '男' | '女' = '男'
pId?: string
readonly id: number
private auth: string = 'view'
constructor(public name: string, public age: number) {
this.id = Math.random()
}
}
const user = new User('zhangsan', 28)
编译结果为:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
this.sex = '男';
this.id = Math.random();
}
}
const user = new User('zhangsan', 28);
泛型
function take (arr, n) {
if (n >= arr.length) {
return arr
}
const newArr = []
for (let i = 0; i < n; i++) {
newArr.push(arr[i])
}
return newArr
}
const newArr = take([1, 2, 3, 4, 5], 2)
console.log(newArr)
上述代码可以正常运行,但是没有添加任何ts代码,作为ts开发来说不够完善。但是给take
函数的arr
参数进行定义时,无法确定它的每一位是什么类型的。如果使用any类型,无法保证传入的arr
参数、内部的newArr
、函数的返回值表示同一个类型。假设我们需要遍历newData
的内容,在书写代码时我们将无法确定newData
的每一位是什么类型,相当于丢失了一些有关联的类型信息。
function take (arr: any[], n: number): any[] {
if (n >= arr.length) {
return arr
}
const newArr: any = []
for (let i = 0; i < n; i++) {
newArr.push(arr[i])
}
return newArr
}
const newData = take([1, 2, 3, 4, 5], 2)
newData.forEach((item: string) => {})
console.log(newData)
泛型:是指附属于函数、类、接口、类型别名之上的类型。使用时在函数、类、接口、类型别名的名称之后加上<泛型名>。不会出现在编译结果中。
function take<T> (arr: T[], n: number): T[] {
if (n >= arr.length) {
return arr
}
const newArr: T[] = []
for (let i = 0; i < n; i++) {
newArr.push(arr[i])
}
return newArr
}
const newData = take<number>([1, 2, 3, 4, 5], 2)
newData.forEach((item: string) => {})
console.log(newData)
即使在调用函数时,不写,ts也能自动识别到传入的数组的类型,从而约束之后使用这个参数时的类型。 在类型别名中使用泛型:
type Callback<T> = (n: T, i: number) => boolean
function filter<T>(arr: T[], callback: Callback<T>): T[] {
const newArr: T[] = []
arr.forEach((n, i) => {
if (callback(n, i)) {
newArr.push(n)
}
})
return newArr
}
const data = [3, 4, 5, 6]
console.log(filter(data, n => n % 2 !== 0))
在接口中使用泛型:
interface Callback<T>{
(n: T, i: number): boolean
}
function filter<T>(arr: T[], callback: Callback<T>): T[] {
const newArr: T[] = []
arr.forEach((n, i) => {
if (callback(n, i)) {
newArr.push(n)
}
})
return newArr
}
const data = [3, 4, 5, 6]
console.log(filter(data, n => n % 2 !== 0))
在类中使用泛型:
class User<T> {
name: string;
private _age: T;
sex: "男" | "女";
constructor(name: string, sex: "男" | "女" = "男") {
this.name = name;
this.sex = sex;
}
setAge(value: T) {
this._age = value
}
getAge() {
return this._age
}
}
const user = new User("zhangsan");
user.setAge(28)
console.log(user.getAge())
泛型约束
interface hasName {
name: string
}
function nameToUppercase<T extends hasName>(obj: T): T {
obj.name = obj.name.split(' ').map(s => s[0].toUpperCase() + s.substr(1)).join(' ')
return obj
}
const data = {
name: 'zhang san',
age: 28,
sex: '男'
}
const newData = nameToUppercase(data)
console.log(newData.name)
一个泛型要求必须包含某些类型
多泛型
// 将两个数组进行混合 [1, 2, 3]与['a', 'b', 'c']进行混合 得到[1, 'a', 2, 'b', 3, 'c']
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
if (arr1.length !== arr2.length) {
throw new Error('两个数组长度不相等')
}
let res: (T | K)[] = []
for (let i = 0; i < arr1.length; i++) {
res.push(arr1[i])
res.push(arr2[i])
}
return res
}
ts模块化
模块化相关配置
配置名称 | 含义 |
---|---|
module | 设置编译结果中使用的模块化标准 |
moduleResolution | 设置解析模块的模式 |
noImplicitUseStrict | 编译结果中不包含“use strict” |
removeComments | 编译结果移除注释 |
noEmitOnError | 错误时不生成编译结果 |
esModuleInterop | 启用es模块化交互非es模块导出 |
ts中如何书写模块化语句 | |
ts在es6的模块化标准出现之前,有自己的单独的一套模块化语句,在es6出现之后,统一使用es6的模块化标准。 | |
新建一个文件module.ts |
在index.ts
中引入(注意:引入时不能加文件后缀,即’./module.ts’
,因为编译后不会存在ts文件,无法找到这个文件):
即使不写import { num, sum } from './module'
,在使用的地方也可以快速引入
点击快速修复
可以直接在有这个声明的文件快速导入
但是这种快速导入只作用于一般的导出,例如上述例子中,直接导出一个声明,export default是无法快速导入的
因为默认导出是没有名称的,在引入时可以随意修改名称,所以ts无法识别
转载自:https://juejin.cn/post/7216252920768921660