TypeScript基础
第一章 快速入门
TypeScript简介
对比于JS,TS是JS的超集,简单的说就是在 JavaScript
的基础上加入了类型系统,让每个参数都有明确的意义,从而带来了更加智能的提示。
相对于JS
而言,TS
属于强类型语言,所以对于项目而言,会使代码更加规范,从而解决了大型项目代码的复杂性,其次,浏览器是不识别TS
的,所以在编译的时候,TS
文件会先编译为JS
文件。
- TypeScript是JavaScript的超集。
- 它对JS进行了扩展,向JS中引入了类型的概念,并添加了许多新的特性。
- TS代码需要通过编译器编译为JS,然后再交由JS解析器执行。
- TS完全兼容JS,换言之,任何的JS代码都可以直接当成TS使用。
- 相较于JS而言,TS拥有了静态类型,更加严格的语法,更强大的功能;TS可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS。
TypeScript 开发环境搭建
下载Node.js
安装Node.js
使用npm全局安装typescript
- 进入命令行
- 输入:
npm i -g typescript
创建一个ts文件
使用tsc对ts文件进行编译
进入命令行
进入ts文件所在目录
执行命令:
tsc xxx.ts
基本类型
类型声明
类型声明是TS非常重要的一个特点
通过类型声明可以指定TS中变量(参数、形参)的类型
指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值
语法:
1
2
3
4
5
6
7let 变量: 类型;
let 变量: 类型 = 值;
function fn(参数: 类型, 参数: 类型): 类型{
...
}
自动类型判断
- TS拥有自动的类型判断机制
- 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型
- 所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明
类型:
类型 例子 描述 number 1, -33, 2.5 任意数字 string ‘hi’, “hi” 任意字符串 boolean true、false 布尔值true或false 字面量 其本身 限制变量的值就是该字面量的值 any * 任意类型 unknown * 类型安全的any void 空值(undefined) 没有值(或undefined) never 没有值 不能是任何值 object {name:’孙悟空’} 任意的JS对象 array [1,2,3] 任意JS数组 tuple [4,5] 元组,TS新增类型,固定长度数组 enum enum{A, B} 枚举,TS中新增类型 number
1
2
3
4
5let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;boolean
1
let isDone: boolean = false;
string
1
2
3
4
5
6
7
8let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}.
I'll be ${age + 1} years old next month.`;字面量
也可以使用字面量去指定变量的类型,通过字面量可以确定变量的取值范围
使用
|
来连接多个类型(联合类型)目前支持
字符串
、数字
、布尔
三种类型。1
2let color: 'red' | 'blue' | 'black';
let num: 1 | 2 | 3 | 4 | 5;
any
表示的是任意类型,一个变量设置类型为any后相当于对该变量关闭了TS的类型检测
使用TS时,不建议使用any类型
声明变量如果不指定类型,则TS解析器会自动判断变量的类型为any (隐式的any)
1
2
3let d: any = 4;
d = 'hello';
d = true;
unknown
类型是any,它可以赋值给任意变量
unknown 实际上就是一个类型安全的any
unknown类型的变量,不能直接赋值给其他变量
1
2let notSure: unknown = 4;
notSure = 'hello';
null
和undefined
两个类型一旦赋值上,就不能在赋值给任何其他类型void
1
let unusable: void = undefined;
never
表示一个函数永远不存在返回值,TS会认为类型为
never
,那么与void
相比,never
应该是void
子集, 因为void
实际上的返回值为undefined
,而never
连undefined
也不行符合
never
的情况有:当抛出异常的情况和无限死循环1
2
3function error(message: string): never {
throw new Error(message);
}object
{} 用来指定对象中可以包含哪些属性
语法:
{属性名:属性值,属性名:属性值}
在属性名后边加上
?
,表示属性是可选的[propName: string]: any
表示任意类型的属性设置函数结构的类型声明:语法:
(形参:类型, 形参:类型 ...) => 返回值
- 在书写的时候,也可以写入返回值的类型,如果写入,则必须要有对应类型的返回值,但通常情况下是省略,因为
TS
的类型推断功能够正确推断出返回值类型 - 参数类型
- 可选参数: 如果函数要配置可有可无的参数时,可以通过
?
实现,可选参数一定要在最后面 - 默认参数:函数内可以自己设定其默认参数,用
=
实现 - 剩余参数:仍可以使用扩展运算符
...
- 可选参数: 如果函数要配置可有可无的参数时,可以通过
- 在书写的时候,也可以写入返回值的类型,如果写入,则必须要有对应类型的返回值,但通常情况下是省略,因为
Object
(大写的O),代表所有的原始类型或非原始类型都可以进行赋值,除了null
和undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let b: {name: string, age?: number};
b = {name: '孙悟空', age: 18};
let c: {name: string, [propName: string]: any};
c = {name: '猪八戒', age: 18, gender: '男'};
let d: (a: number ,b: number)=>number;
let obj: Object;
obj = 1; // ok
obj = "a"; // ok
obj = true; // ok
obj = {}; // ok
obj = Symbol() //ok
obj = 10n //ok
obj = null; // error
obj = undefined; // error
array
类型[]
Array<类型>
1
2let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
tuple
元组就是固定长度的数组
语法:
[类型, 类型, 类型]
1
2let x: [string, number];
x = ["hello", 10];
enum
可以定义一些带名字的常量,这样可以清晰表达意图或创建一组有区别的用例
注意:
- 枚举的类型只能是
string
或number
- 定义的名称不能为关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;
enum Color {
Red = 1,
Green,
Blue,
}
let c: Color = Color.Green;
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
let c: Color = Color.Green;- 枚举的类型只能是
类型断言
有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:
第一种
1
2let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;第二种
1
2let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;
补充
&表示同时 交叉类型:将多个类型合并为一个类型,使用
&
符号连1
2let j: { name: string } & { age: number };
// j = {name: '孙悟空', age: 18};类型的别名
1
2
3
4
5
6type myType = 1 | 2 | 3 | 4 | 5;
let k: myType;
let l: myType;
let m: myType;
k = 2;
编译选项
自动编译文件
编译文件时,使用 -w 指令后,TS编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译。
示例:
1
tsc xxx.ts -w
自动编译整个项目
如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译为js文件。
但是能直接使用tsc命令的前提时,要先在项目根目录下创建一个ts的配置文件 tsconfig.json
tsconfig.json
是一个JSON文件,添加配置文件后,只需只需tsc
命令即可完成对整个项目的编译配置选项:
include
定义希望被编译文件所在的目录
默认值:[“
**表示任意目录
/*表示任意文件
“]示例:
1
"include":["src/**/*", "tests/**/*"]
上述示例中,所有src目录和tests目录下的文件都会被编译
exclude
定义需要排除在外的目录
默认值:[“node_modules”, “bower_components”, “jspm_packages”]
示例:
1
"exclude": ["./src/hello/**/*"]
上述示例中,src下hello目录下的文件都不会被编译
extends
定义被继承的配置文件
示例:
1
"extends": "./configs/base"
上述示例中,当前配置文件中会自动包含config目录下base.json中的所有配置信息
files
指定被编译文件的列表,只有需要编译的文件少时才会用到
示例:
1
2
3
4
5
6
7
8
9
10
11"files": [
"core.ts",
"sys.ts",
"types.ts",
"scanner.ts",
"parser.ts",
"utilities.ts",
"binder.ts",
"checker.ts",
"tsc.ts"
]列表中的文件都会被TS编译器所编译
compilerOptions
编译选项是配置文件中非常重要也比较复杂的配置选项
在compilerOptions中包含多个子选项,用来完成对编译的配置
项目选项
target
设置ts代码编译的目标版本
可选值:
- ES3(默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext
示例:
1
2
3"compilerOptions": {
"target": "es6"
}如上设置,我们所编写的ts代码将会被编译为ES6版本的js代码
lib
指定代码运行时所包含的库(宿主环境)
可选值:
- ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost ……
示例:
1
2
3
4
5
6"compilerOptions": {
"target": "ES6",
"lib": ["ES6", "DOM"],
"outDir": "dist",
"outFile": "dist/aa.js"
}
module
设置编译后代码使用的模块化系统
可选值:
- CommonJS、UMD、AMD、System、ES2020、ESNext、None
示例:
1
2
3"compilerOptions": {
"module": "CommonJS"
}
outDir
编译后文件的所在目录
默认情况下,编译后的js文件会和ts文件位于相同的目录,设置outDir后可以改变编译后文件的位置
示例:
1
2
3"compilerOptions": {
"outDir": "dist"
}设置后编译后的js文件将会生成到dist目录
outFile
将所有的文件编译为一个js文件
默认会将所有的编写在全局作用域中的代码合并为一个js文件,如果module制定了None、System或AMD则会将模块一起合并到文件之中
示例:
1
2
3"compilerOptions": {
"outFile": "dist/app.js"
}
rootDir
指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录
示例:
1
2
3"compilerOptions": {
"rootDir": "./src"
}
allowJs
- 是否对js文件编译
checkJs
是否对js文件进行检查
示例:
1
2
3
4"compilerOptions": {
"allowJs": true,
"checkJs": true
}
removeComments
- 是否删除注释
- 默认值:false
noEmit
- 不对代码进行编译
- 默认值:false
sourceMap
- 是否生成sourceMap
- 默认值:false
严格检查
strict
- 启用所有的严格检查,默认值为true,设置后相当于开启了所有的严格检查
alwaysStrict
- 用来设置编译后的文件是否使用严格模式,默认false
noImplicitAny
- 禁止隐式的any类型
noImplicitThis
- 禁止类型不明确的this
- strictBindCallApply
- 严格检查bind、call和apply的参数列表
- strictFunctionTypes
- 严格检查函数的类型
strictNullChecks
- 严格的空值检查
- strictPropertyInitialization
- 严格检查属性是否初始化
额外检查
- noFallthroughCasesInSwitch
- 检查switch语句包含正确的break
- noImplicitReturns
- 检查函数没有隐式的返回值
- noUnusedLocals
- 检查未使用的局部变量
- noUnusedParameters
- 检查未使用的参数
- noFallthroughCasesInSwitch
高级
- allowUnreachableCode
- 检查不可达代码
- 可选值:
- true,忽略不可达代码
- false,不可达代码将引起错误
noEmitOnError
- 有错误的情况下不进行编译
- 默认值:false
- allowUnreachableCode
webpack
通常情况下,实际开发中我们都需要使用构建工具对代码进行打包,TS同样也可以结合构建工具一起使用,下边以webpack为例介绍一下如何结合构建工具使用TS。
步骤:
初始化项目
- 进入项目根目录,执行命令
npm init -y
- 主要作用:创建package.json文件
- 进入项目根目录,执行命令
下载构建工具
npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader html-webpack-plugin clean-webpack-plugin
- 共安装了7个包
- webpack
- 构建工具webpack
- webpack-cli
- webpack的命令行工具
- webpack-dev-server
- webpack的开发服务器
- typescript
- ts编译器
- ts-loader
- ts加载器,用于在webpack中编译ts文件
- html-webpack-plugin
- webpack中html插件,用来自动创建html文件
- clean-webpack-plugin
- webpack中的清除插件,每次构建都会先清除目录
- webpack
- 共安装了7个包
根目录下创建webpack的配置文件webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
optimization:{
minimize: false // 关闭代码压缩,可选
},
entry: "./src/index.ts",
devtool: "inline-source-map",
devServer: {
contentBase: './dist'
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
environment: {
arrowFunction: false // 关闭webpack的箭头函数,可选
}
},
resolve: {
extensions: [".ts", ".js"]
},
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: "ts-loader"
},
exclude: /node_modules/
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
//title:'TS测试'
template: "./src/index.html"
}),
]
}根目录下创建tsconfig.json,配置可以根据自己需要
1
2
3
4
5
6
7{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"strict": true
}
}修改package.json添加如下配置
1
2
3
4
5
6
7
8
9{
...略...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open"
},
...略...
}在src下创建ts文件,并在并命令行执行
npm run build
对代码进行编译,或者执行npm start
来启动开发服务器
Babel
经过一系列的配置,使得TS和webpack已经结合到了一起,除了webpack,开发中还经常需要结合babel来对代码进行转换以使其可以兼容到更多的浏览器,在上述步骤的基础上,通过以下步骤再将babel引入到项目中。
安装依赖包:
npm i -D @babel/core @babel/preset-env babel-loader core-js
- 共安装了4个包,分别是:
- @babel/core
- babel的核心工具
- @babel/preset-env
- babel的预定义环境
- @babel-loader
- babel在webpack中的加载器
- core-js
- core-js用来使老版本的浏览器支持新版ES语法
- @babel/core
修改webpack.config.js配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85// 引入一个包
const path = require('path');
// 引入html插件
const HTMLWebpackPlugin = require('html-webpack-plugin');
// 引入clean插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// webpack中的所有的配置信息都应该写在module.exports中
module.exports = {
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后文件的文件
filename: "bundle.js",
// 告诉webpack不使用箭头
environment:{
arrowFunction: false
}
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader:"babel-loader",
// 设置babel
options: {
// 设置预定义的环境
presets:[
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets:{
"chrome":"58",
"ie":"11"
},
// 指定corejs的版本
"corejs":"3",
// 使用corejs的方式 "usage" 表示按需加载
"useBuiltIns":"usage"
}
]
]
}
},
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
}
]
},
// 配置Webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title: "这是一个自定义的title"
template: "./src/index.html"
}),
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js']
}
};如此一来,使用ts编译后的文件将会再次被babel处理,使得代码可以在大部分浏览器中直接使用,可以在配置选项的targets中指定要兼容的浏览器版本。
第二章:面向对象
面向对象是程序中一个非常重要的思想,它被很多同学理解成了一个比较难,比较深奥的问题,其实不然。面向对象很简单,简而言之就是程序之中所有的操作都需要通过对象来完成。
- 举例来说:
- 操作浏览器要使用window对象
- 操作网页要使用document对象
- 操作控制台要使用console对象
一切操作都要通过对象,也就是所谓的面向对象,那么对象到底是什么呢?这就要先说到程序是什么,计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体,比如:照片是对一个具体的人的抽象,汽车模型是对具体汽车的抽象等等。程序也是对事物的抽象,在程序中我们可以表示一个人、一条狗、一把枪、一颗子弹等等所有的事物。一个事物到了程序中就变成了一个对象。
在程序中所有的对象都被分成了两个部分:数据和功能,以人为例,人的姓名、性别、年龄、身高、体重等属于数据,人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被成为属性,而功能就被称为方法。所以简而言之,在程序中一切皆是对象。
类(class)
要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。要创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象,举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过Car类来创建汽车的对象,不同的类可以用来创建不同的对象。
定义类:
1
2
3
4
5
6
7
8
9
10
11
12class 类名 {
属性名: 类型;
constructor(参数: 类型){
this.属性名 = 参数;
}
方法名(){
....
}
}示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Person{
//静态属性
static name1: string = 'Domesy'
//成员属性,实际上是通过public上进行修饰,只是省略了
// 需要注意的是: 在成员属性中,如果不给默认值,并且不使用是会报错的,如果不想报错就给加!
name: string;
age: number;
money!: number;
// constructor 被称为构造函数
// 构造函数会在对象创建时调用
constructor(name: string, age: number){
// 在实例方法中,this就表示当前当前的实例
// 在构造函数中当前对象就是当前新建的那个对象
// 可以通过this向新建的对象中添加属性
this.name = name;
this.age = age;
}
sayHello(){
// 在方法中可以通过this来表示当前调用方法的对象
console.log(`大家好,我是${this.name}`);
}
}使用类:
1
2const p = new Person('孙悟空', 18);
p.sayHello();
面向对象的特点
封装
对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装
默认情况下,对象的属性是可以任意的修改的,为了确保数据的安全性,在TS中可以对属性的权限进行设置
私有字段(
#
)在 TS 3.8版本便开始支持ECMACMAScript的私有字段。
需要注意的是
私有字段
与常规字段不同,主要的区别是:- 私有字段以
#
字符开头,也叫私有名称; - 每个私有字段名称都唯一地限定于其包含的类;
- 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
- 私有字段不能在包含的类之外访问,甚至不能被检测到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Info {
#name: string; //私有字段
getName: string;
constructor(name: string) {
this.#name = name;
this.getName = name
}
setName() {
return `我的名字是${this.#name}`
}
}
let myName = new Info("Domesy");
console.log(myName.setName()) // "我的名字是Domesy"
console.log(myName.getName) // ok "Domesy"
console.log(myName.#name) // error
// Property '#name' is not accessible outside class 'Info'
// because it has a private identifier.(18013)- 私有字段以
只读属性(readonly):
- 如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改
- 用
readonly
修饰,只能在构造函数中初始化,并且在TS中,只允许将interface
、type
、class
上的属性标识为readonly
readonly
实际上只是在编译阶段
进行代码检查- 被
radonly
修饰的词只能在constructor
阶段修改,其他时刻不允许修改
TS中属性具有三种修饰符:
public(默认值),类中、子类内的任何地方、外部都能调用
protected ,类中、子类内的任何地方都能调用,但外部不能调用
private ,类中可以调用,子类内的任何地方、外部均不可调用
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class Person {
public name: string
protected age: number
private tel: number
constructor(name: string, age:number, tel: number){
this.name = name
this.age = age
this.tel = tel
}
}
class Child extends Person {
constructor(name: string, age: number, tel: number) {
super(name, age, tel);
}
getName(){
console.log(`我的名字叫${this.name},年龄是${this.age}`) // ok name 和 age可以
console.log(`电话是${this.tel}`) // error 报错 原因是 tel 拿不出来
}
}
const res = new Child('Domesy', 7, 123456)
console.log(res.name) // ok Domesy
console.log(res.age) // error
console.log(res.tel) // error
属性存取器
对于一些不希望被任意修改的属性,可以将其设置为private
直接将其设置为private将导致无法再通过对象修改其中的属性
我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器
设置属性的方法叫做
setter
方法,读取属性的方法叫做getter
方法示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55class Person{
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
}
/*
* getter方法用来读取属性
* setter方法用来设置属性
* - 它们被称为属性的存取器
* */
// 定义方法,用来获取name属性
// getName(){
// return this._name;
// }
//
// // 定义方法,用来设置name属性
// setName(value: string){
// this._name = value;
// }
// TS中设置getter方法的方式
get name(){
// console.log('get name()执行了!!');
return this._name;
}
set name(value){
this._name = value;
}
get age(){
return this._age;
}
set age(value){
if(value >= 0){
this._age = value
}
}
}
const per = new Person('孙悟空', 18);
// per.setName('猪八戒');
// per.setAge(-33);
per.name = '猪八戒';
per.age = -33;
静态属性
静态属性(方法),也称为类属性。使用静态属性无需创建实例,通过类即可直接使用
静态属性(方法)使用
static
开头示例:
1
2
3
4
5
6
7
8
9
10class Tools{
static PI = 3.1415926;
static sum(num1: number, num2: number){
return num1 + num2
}
}
console.log(Tools.PI);
console.log(Tools.sum(123, 456));
this
- 在类中,使用this表示当前对象
继承
继承是面向对象中的又一个特性
通过继承可以将其他类中的属性和方法引入到当前类中
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Animal{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
class Dog extends Animal{
bark(){
console.log(`${this.name}在汪汪叫!`);
}
}
const dog = new Dog('旺财', 4);
dog.bark();
通过继承可以在不修改类的情况下完成对类的扩展
重写和重载
重写:子类重写继承自父类中的方法
重载:指为同一个函数提供多个类型定义
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// 重写
class Person{
setName(name: string){
return `我的名字叫${name}`
}
}
class Child extends Person{
setName(name: string){
return `你的名字叫${name}`
}
}
const yourName = new Child()
console.log(yourName.setName('小杜杜')) // "你的名字叫小杜杜"
// 重载
class Person1{
setNameAge(name: string):void;
setNameAge(name: number):void;
setNameAge(name:string | number){
if(typeof name === 'string'){
console.log(`我的名字是${name}`)
}else{
console.log(`我的年龄是${name}`)
}
};
}
const res = new Person1()
res.setNameAge('小杜杜') // "我的名字是小杜杜"
res.setNameAge(7) // "我的年龄是7"在子类中可以使用super来完成对父类的引用
- 如果在子类中写了构造函数,在子类构造函数中必须对父类的构造函数进行调用
- 在类的方法中 super就表示当前类的父类
抽象类(abstract class)
用abstract关键字声明的类叫做抽象类,声明的方法叫做抽象方法
- 抽象类:指不能被实例化,因为它里面包含一个或多个抽象方法。
- 抽象方法:是指不包含具体实现的方法;
注:抽象类是不能直接实例化,只能实例化实现了所有抽象方法的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract setAge(age: number) :void;
}
class Child extends Person {
constructor(name: string) {
super(name);
}
setAge(age: number): void {
console.log(`我的名字是${this.name},年龄是${age}`);
}
}
let res = new Person("小杜杜") //error
let res1 = new Child("小杜杜");
res1.setAge(7) // "我的名字是小杜杜,年龄是7"
接口(Interface)
接口的作用类似于抽象类,不同点在于接口中的所有方法和属性都是没有实值的,换句话说接口中的所有方法都是抽象方法。接口主要负责定义一个类的结构,接口可以去限制一个对象的接口,对象只有包含接口中定义的所有属性和方法时才能匹配接口。同时,可以让一个类去实现接口,实现接口时类中要保护接口中的所有属性。
示例(检查对象类型):
1
2
3
4
5
6
7
8
9
10
11interface Person{
name: string;
sayHello():void;
}
function fn(per: Person){
per.sayHello();
}
fn({name:'孙悟空', sayHello() {console.log(`Hello, 我是 ${this.name}`)}});示例(实现)
1
2
3
4
5
6
7
8
9
10
11
12
13interface Person{
name: string;
sayHello():void;
}
class Student implements Person{
constructor(public name: string) {
}
sayHello() {
console.log('大家好,我是'+this.name);
}
}
泛型(Generic)
定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型便能够发挥作用。
举个例子:
1
2
3function test(arg: any): any{
return arg;
}上例中,test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的,由于类型不确定所以参数和返回值均使用了any,但是很明显这样做是不合适的,首先使用any会关闭TS的类型检查,其次这样设置也不能体现出参数和返回值是相同的类型
使用泛型:
1
2
3function test<T>(arg: T): T{
return arg;
}这里的
<T>
就是泛型,T是我们给这个类型起的名字(不一定非叫T),设置泛型后即可在函数中使用T来表示该类型。所以泛型其实很好理解,就表示某个类型。那么如何使用上边的函数呢?
方式一(直接使用):
1
test(10)
使用时可以直接传递参数使用,类型会由TS自动推断出来,但有时编译器无法自动推断时还需要使用下面的方式
方式二(指定类型):
1
test<number>(10)
也可以在函数后手动指定泛型
可以同时指定多个泛型,泛型间使用逗号隔开:
1
2
3
4
5function test<T, K>(a: T, b: K): K{
return b;
}
test<number, string>(10, "hello");使用泛型时,完全可以将泛型当成是一个普通的类去使用
类中同样可以使用泛型:
1
2
3
4
5
6
7class MyClass<T>{
prop: T;
constructor(prop: T){
this.prop = prop;
}
}除此之外,也可以对泛型的范围进行约束
1
2
3
4
5
6
7interface MyInter{
length: number;
}
function test<T extends MyInter>(arg: T): number{
return arg.length;
}使用T extends MyInter表示泛型T必须是MyInter的子类,不一定非要使用接口类和抽象类同样适用。
TS断言和类型守卫
TS断言
分为三种:类型断言
、非空断言
、确定赋值断言
当断言失效后,可能使用到:双重断言
类型断言
在特定的环境中,我们会比TS知道这个值具体是什么类型,不需要TS去判断,简单的理解就是,类型断言会告诉编译器,你不用给我进行检查,相信我,他就是这个类型
共有两种方式:
- 尖括号
- as:推荐
1 | //尖括号 |
但需要注意的是:尖括号语法在React中会报错,原因是与JSX
语法会产生冲突,所以只能使用as语法
非空断言
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !
可以用于断言操作对象是非 null 和非 undefined 类型。
具体而言,x! 将从 x 值域中排除 null 和 undefined 。
那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些使用场景。
忽略 undefined 和 null 类型
1 | function myFunc(maybeString: string | undefined | null) { |
调用函数时忽略 undefined 类型
1 | type NumGenerator = () => number; |
因为 !
非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:
1 | const a: number | undefined = undefined; |
以上 TS 代码会编译生成以下 ES5 代码:
1 | ; |
虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!;
语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,!
非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined
。
确定赋值断言
在TS
2.7版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 !
号,以告诉TS
该属性会被明确赋值。
为了更好地理解它的作用,我们来看个具体的例子:
1 | let x: number; |
很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
1 | let x!: number; |
通过 let x!: number;
确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
双重断言
断言失效后,可能会用到,但一般情况下不会使用
失效的情况:基础类型不能断言为接口
1 | interface Info{ |
类型守卫
类型守卫:是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。
我个人的感觉是,类型守卫就是你可以设置多种类型,但我默认你是什么类型的意思
目前,常有的类型守卫共有4种:in关键字、typeof关键字、instanceof和类型谓词(is)
in关键字
用于判断这个属性是那个里面的
1 | interface Info { |
typeof关键字
用于判断基本类型,如string | number等
1 | const setInfo = (data: number | string | undefined) => { |
instanceof关键字
用于判断一个实例是不是构造函数,或使用类的时候
1 | class Name { |
类型谓词(is)
1 | function isNumber(x: any): x is number { //默认传入的是number类型 |
两者的区别
通过上面的介绍,我们可以发现断言与类型守卫的概念非常相似,都是确定参数的类型,但断言更加霸道,它是直接告诉编辑器,这个参数就是这个类型,而类型守卫更像确定这个参数具体是什么类型。
类型别名、接口
类型别名(type)
类型别名:也就是type
,用来给一个类型起个新名字
1 | type InfoProps = string | number |
接口(interface)
接口:在面向对象语言中表示行为抽象,也可以用来描述对象的形状。
使用interface关键字来定义接口
对象的形状
接口可以用来描述对象
,主要可以包括以下数据:可读属性
、只读属性
、任意属性
- 可读属性:当我们定义一个接口时,我们的属性可能不需要全都要,这是就需要
?
来解决 - 只读属性:用 readonly修饰的属性为只读属性,意思是指允许定义,不允许之后进行更改
- 任意属性:这个属性极为重要,它是可以用作就算没有定义,也可以使用,比如 [propName: string]: any。比如说我们对组件进行封装,而封装的那个组件并没有导出对应的类型,然而又想让他不报错,这时就可以使用任意属性
1 | interface Props { |
继承
继承:与类一样,接口也存在继承属性,也是使用extends
字段
1 | interface nameProps { |
函数类型接口
同时,可以定义函数和类,加new
修饰的事类,不加new的事函数
1 | interface Props { |
type 和 interface 的区别
通过上面的学习,我们发现类型别名
和接口
非常相似,可以说在大多数情况下,type
与interface
是等价的
但在一些特定的场景差距还是比较大的,接下来逐个来看看
基础数据类型
type
和interface
都可以定义 对象 和 函数type
可以定义其他数据类型,如字符串、数字、元祖、联合类型等,而interface
不行
1 | type A = string // 基本类型 |
扩展
interface
可以扩展 type
,type
也可以扩展为 interface
,但两者实现扩展的方式不同。
interface
是通过extends
来实现type
是通过&
来实现
1 | // interface 扩展 interface |
重复定义
interface
可以多次被定义,并且会进行合并,但type
不行
1 | interface A { |
联合类型(Union Types)
联合类型(Union Types): 表示取值可以为多种类型中的一种,未赋值时联合类型上只能访问两个类型共有的属性和方法,如:
1 | const setInfo = (name: string | number) => {} |
从上面看 setInfo
接收一个name
,而 name
可以接收 string
或number
类型,那么这个参数便是联合类型
可辨识联合
可辨识联合:包含三个特点,分别是可辨识
、联合类型
、类型守卫
,
这种类型的本质是:结合联合类型和字面量类型的一种类型保护方法。
如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
也就是上面一起结合使用,这里写个小例子:
1 | interface A { |
定义了 A
、B
、C
三次接口,但这三个接口都包含type
属性,那么type
就是可辨识的属性
,而其他属性只跟特性的接口相关。
然后通过可辨识属性type
,才能使用其相关的属性
泛型
泛型:Generics,是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
也就是说,泛型是允许同一个函数接受不同类型参数的一种模版,与any
相比,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型(PS:泛型是整个TS的重点,也是难点,请多多注意~)
为什么需要泛型
我们先看看一个例子:
1 | const calcArray = (data:any):any[] => { |
上述的例子我们发现,在calcArray
中传任何类型的参数,返回的数组都是any
类型
由于我们不知道传入的数据是什么,所以返回的数据也为any的数组
但我们现在想要的效果是:无论我们传什么类型,都能返回对应的类型,针对这种情况怎么办?所以此时泛型
就登场了
泛型语法
我们先用泛型对上面的例子进行改造下,
1 | const calcArray = <T>(data:T):T[] => { |
经过上面的案例,我们发现传入的字符串
、数字
、对象
,都能返回对应的类型,从而达到我们的目的,接下来我们再看看泛型语法
:
1 | function identity <T>(value:T) : T { |
第一次看到这个<T>
我们是不是很懵,实际上这个T
就是传递的类型,从上述的例子来看,这个<T>
就是<string>
,要注意一点,这个<string>
实际上是可以省略的,因为 TS 具有类型推论,可以自己推断类型
多类型传参
我们有多个未知的类型占位,我们可以定义任何的字母来表示不同的参数类型
1 | const calcArray = <T,U>(name:T, age:U): {name:T, age:U} => { |
泛型接口
定义接口的时候,我们也可以使用泛型
1 | interface A<T> { |
泛型类
同样泛型也可以定义类
1 | class clacArray<T>{ |
泛型类型别名
1 | type Info<T> = { |
泛型默认参数
所谓默认参数,是指定类型,如默认值一样,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。
1 | const calcArray = <T = string,>(data:T):T[] => { |
泛型常用字母
用常用的字母来表示一些变量的代表:
- T:代表Type,定义泛型时通常用作第一个类型变量名称
- K:代表Key,表示对象中的键类型;
- V:代表Value,表示对象中的值类型;
- E:代表Element,表示的元素类型;
常用技巧
在 TS 中有许多关键字和工具类型,在使用上,需要注意泛型上的应用,有的时候结合起来可能就有一定的问题
在此特别需要注意 extends
、typeof
、Partial
、Record
、Exclude
、Omit
这几个工具类型
extends
extends:检验是否拥有其属性 在这里,举个例子,我们知道字符串
和数组
拥有length
属性,但number
没有这个属性。
1 | const calcArray = <T,>(data:T): number => { |
上述的 calcArray
的作用只是获取data的数量
,但此时在TS
中会报错,这是因为TS不确定传来的属性是否具备length这个属性,毕竟每个属性都不可能完全相同
那么这时该怎么解决呢?
我们已经确定,要拿到传过来数据的 length
,也就是说传过来的属性必须具备length
这个属性,如果没有,则不让他调用这个方法。
换句话说,calcArray
需要具备检验属性的功能,对于上述例子就是检验是否有length
的功能,这是我们就需要extends这个属性帮我们去鉴定:
1 | interface Props { |
可以看出calcArray(2)
会报错,这是因为number
类型并不具备length
这个属性
typeof
typeof关键字:我们在类型保护的时候讲解了typeof的作用,除此之外,这个关键字还可以实现推出类型。
1 | let info ={ |
keyof
keyof关键字: 可以获取一个对象接口的所有key
值,可以检查对象上的键是否存在
1 | interface Props { |
索引访问操作符
索引访问操作符:通过 []
操作符可进行索引访问,可以访问其中一个属性
1 | interface Props { |
in
in:映射类型, 用来映射遍历枚举类型
1 | type namme ='name' | 'age' | 'sex' |
infer
infer:可以是使用为条件语句,可以用 infer
声明一个类型变量并且对它进行使用。如
1 | type Info<T> = T extends { a: infer U; b: infer U } ? U : never; |
Partial
Partial语法:Partial<T>
作用:将所有属性变为可选的 ?
1 | interface Props { |
从上述代码上来看,name 和 age 属于必填,对于 info 来说必须要设置 name 和 age 属性才行,但对于 info1来说,只要是个对象就可以,至于是否有name、 age属性并不重要
Required
Required语法:Required<T>
作用:将所有属性变为必选的,与 Partial
相反
1 | interface Props { |
Readonly
Readonly语法:Readonly<T>
作用:将所有属性都加上 readonly 修饰符来实现。也就是说无法修改
1 | interface Props { |
从上述代码上来看, Readonly
修饰后,属性无法再次更改,智能使用
Record
Record语法:Record<K extends keyof any, T>
作用:将 K
中所有的属性的值转化为 T
类型。
1 | interface Props { |
从上述代码上来看, InfoProps
的属性分别包含Props
的属性
需要注意的一点是:K extends keyof any
其类型可以是:string
、number
、symbol
Pick
Pick语法:Pick<T, K extends keyof T>
作用:将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。
1 | interface Props { |
从上述代码上来看, Props
原本属性包括name
、age
、sex
三个属性,通过 Pick我们吧name
和age
挑了出来,所以不需要sex
属性
Exclude
Exclude语法:Exclude<T, U>
作用:将T类型中的U类型剔除。
1 | // 数字类型 |
从上述代码上来看,我们比较了下类型上的,当 T 中有 U 就会剔除对应的属性,如果 U 中又的属性 T 中没有,或 T 和 U 刚好一样的情况都会返回 nerver,且对象永远返回nerver
Extra
Extra语法:Extra<T, U>
作用:将T 可分配给的类型中提取 U。与 Exclude相反
1 | type numProps = Extract<1 | 2 | 3, 1 | 2> // 1 | 2 |
Omit
Omit语法:Omit<T, U>
作用:将已经声明的类型进行属性剔除获得新类型
与 Exclude的区别:Omit 返回的是新的类型,原理上是在 Exclude
之上进行的,Exclude
是根据自类型返回的
NonNullable
NonNullable语法:NonNullable<T>
作用:从 T 中排除 null
和 undefined
1 | type Props = NonNullable<string | undefined |null> |
ReturnType
ReturnType语法:ReturnType<T>
作用:用于获取 函数T的返回类型。
1 | type Props = ReturnType<() => string> // string |
从上述代码上来看, ReturnType可以接受 any 和 never 类型,原因是这两个类型属于顶级类型,包含函数
Parameters
Parameters:Parameters<T>
作用:用于获取 获取函数类型的参数类型
1 | type Props = Parameters<() => string> // [] |