1 TypeScript介绍
1.1 什么是TypeScript?
TypeScript
简称TS,它是以JavaScript为基础构建的语言,是JavaScript的超集
(TS是JS的升级版),本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。 TS的目的是为了更安全高效的类型提示和检查
1.2 为什么需要 TypeScript
JavaScript是弱类型语言,很多错误只有在运行时才会被发现,而TypeScript提供了一套静态检测机制,可以帮助我们在编译时就发现错误 。
1.3 JS与TS的相关知识
JS
是一门标准的弱类型且动态类型的语言,所以JS灵活多变,缺失类型系统的可靠性。
TS
是一门标准的强类型且静态类型的语言。
强类型VS弱类型
强类型
:在语言层面就限制了函数的实参类型必须与形参类型相同
弱类型
:语言层面不会限制实参的类型; 我们定义一个变量,不需要考虑它的类型
形参
相当于函数中定义的变量,实参
是在运行时的函数调用时传入的参数。
强类型语言的特点就是不允许程序在发生错误后继续执行
弱类型的一个特点就是在计算时,不同类型之间对使用者透明地对变量进行隐式转换。
相较于弱类型语言,强类型有着明显的优势
:
- 错误更早暴露(编码阶段就能发现并避免类型异常);
- 代码更智能,编码更准确(编辑器时时刻刻都知道变量的类型,作出提示);
- 重构更牢靠(重构时,能及时暴露出相关代码异常信息,可及时准确的对耦合代码进行修改);
- 减少不必要的类型判断(在编码阶段,参数类型已经确定,就不用再对类型做约定判断);
静态类型VS动态类型
静态类型语言
:一个变量在声明时它的类型就是明确的,声明过后,它的类型就不可修改;
动态类型语言
:在代码运行阶段才能明确变量的类型,而且变量的类型随时可以改变;
两者区别
:是否允许随时修改变量的类型 ,
如何区分
:一门语言在编译时报错,那么是静态类型,如果在运行时报错,那么是动态类型。
静态类型的好处
:有利于代码的重构,可以在编译器编译时捕获错误。这样我们在编写代码的时候就能避免很多错误,提高编码的效率!
静态类型语言 | 动态类型语言 |
---|---|
对类型极度严格 | 对类型非常宽松 |
立即发现错误 | 不能立即发现(单元测试) |
运行时性能好 | 运行时性能差 |
自文档化 | 可读性差(工具生成文档) |
1.4 TypeScript特性
- 支持最新的JavaScript新特性(ES6-ES12),添加ES不具备的新特性
- 支持代码静态检查,拥有了更加严格的语法和更强大的功能 ,TS可以在代码执行前就完成代码的检查,
减小了运行时异常的出现的几率; - 同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS;
- 支持诸如C,C++,Java,Go等后端语言中的特性 (枚举、泛型、类型转换、命名空间、声明文件、类、接口等) ,有强大的开发工具以及丰富的配置选项。
1.5 TypeScript安装
安装Node
:https://nodejs.org/zh-cn/
安装ts
:
npm i -g typescript
将ts文件编译为js文件(生成一个和ts文件名称相同的js文件),然后执行js文件
//编译ts文件
tsc XXX.ts
//运行js文件
node XXX.js
也可以安装ts-node直接运行ts文件 ,不需要编译为js
/安装ts-node
npm i -g ts-node
//直接运行ts文件
ts-node XXX.ts
2 TypeScript的基本类型
2.1 类型声明
类型声明
给变量设置了类型,使得变量只能存储某种类型的值。
通过类型声明可以指定TS中变量(参数字、形参)的类型,指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错。
语法如下:
// 声明一个变量
let 变量: 类型;
// 声明一个变量并给其赋值
let 变量: 类型 = 值;
// 声明一个函数
function fn(参数: 类型, 参数: 类型): 函数返回值类型{
...
}
// a 的类型设置为了number,在以后的使用过程中a的值只能是数字
let a: number;
a = 10;
a = 33;
a = 'hello'; // 此行代码会报错,因为变量a的类型是number,不能赋值字符串
自动类型判断
:TS拥有自动的类型判断机制,当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型,所以变量的声明和赋值时同时进行可以省略掉类型声明。
注
:虽然可以省略类型,但声明类型对定义函数有极大的提升
let a = 1;
a = 'sj';//报错 不能将类型'string'分配给类型'number'
2.2 类型分类
类型 | 例子 | 描述 |
---|---|---|
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中新增类型 |
2.2.1 number
number
:任意
let a: number;
// a 的类型设置为了number,在以后的使用过程中a的值只能是数字
a = 10;
a = 33;
// a = 'hello'; // 此行代码会报错,因为变量a的类型是number,不能赋值字符串
2.2.2 string
string
:任意字符串
let color: string = "blue";
color = 'red';
2.2.3 boolean
boolean
:布尔值true或false
let isDone: boolean = false;
2.2.4 字面量
字面量
:字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。也可以使用字面量去指定变量的类型,通过字面量可以确定变量的取值范围。
let a: 10; //a的值只能为10
//可以使用|来连接多个类型(|为联合类型表示取值可以为多种类型中的一种)
let b: 'male' | 'female'; //b的值只能二选一
b = 'male';
b = 'female';
let c: boolean | string;
c = true;
c = 'sd ';
2.2.5 any
any
:任意类型
一个变量设置为any后相当于对该变量关闭了TS的类型检测,故不建议使用。声明变量如果不指定类型,则TS解析器会自动判断变量的类型为any(隐式的any)。
let d: any = 4;
d = 'hello';
let e ;
e=1;
e=true;
//d的值为any,它可以赋值给任意变量
let s:string
s=d;//这样变量s的类型检测也关闭了
2.2.6 unknown
unknown
:类型安全的any,不能直接赋值给其他变量
let e: unknown;
e="hello"
let f:string
f=e;//报错 类型unkonwn不能赋值给string类型
2.2.7 void
void
:void表示没有任何类型,和其他类型是平等关系,不能直接赋值:
声明一个void类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。
//void用来表示空
function fn(): void {
return undefined//除了undefined的其他类型的返回值会报错
}
2.2.8 never
never
:不能是任何值
//never 表示永远不会返回结果 连undefined都不能返回,一般用来报错
function fn1(): never {
throw new Error('报错了');
}
2.2.9 object
object
:任意的JS对象
//1.定义对象结构的类型声明
/*
语法:{属性名:属性值,属性名:属性值}
在属性名后边加上?,表示属性是可选的
*/
let b: { name: string; age?: number };
b = { name: '孙悟空', age: 18 };
//[xxx:string]:any 表示任意类型的属性
let c: { name: string; [propName: string]: any };
c = { name: '猪八戒', age: 18, gender: '男' };
//2.定义函数结构的类型声明
//我们一般会限制函数有几个参数,参数的类型,返回值的类型,可以以一种类似箭头函数的方式来声明一个函数类型的变量
/*
语法:(形参:类型,形参:类型...) => 返回值
*/
let d: (a: number, b: number) => number;
d = function (n1, n2): number {
return n1 + n2;
};
2.2.10 array
array
:任意JS数组
/*
语法:类型[]或Array<类型>
*/
//如string[]表示字符串数组 (在元素类型后面接上[]) Array<number>表示数字数组
let e: string[]
//or
let e: Array<string>;
2.2.11 tuple
tuple
: 元组,TS新增类型,就是固定长度(数量)数组
元组中包含的元素,必须与声明的类型一致,数量要对应,且顺序也要一一对应
/*
语法:[类型,类型,类型]
*/
let h: [string,string,number];//里面两个字符串类型,一个数字
h = ['s', 'f',1];//必须对应两个字符串,一个数字,顺序也不能变化
2.2.12 enum
比如我们在定义一个对象的时候 let person = { name: ‘zs’, gender: ‘male’ }。像gender
这种属性的取值一般只有两个,所以在存储的时候用数字来代替,可以提高效率,可以定义0
表示女性,1
表示男性。枚举成员如果没有显式指定值,TypeScript 会自动为其分配一个整数值(从 0 开始递增)
enum
:枚举,TS中新增类型,把所有可能的情况全部列出来,适合用于多个值进行选择的场景,
// 基本枚举定义,自动分配数字值
enum Color {
Red,
Green,
Blue,
}
// 输出:
// Color.Red === 0
// Color.Green === 1
// Color.Blue === 2
// 自定义枚举成员值
enum ColorWithCustomValues {
Red = 1,
Green = 2,
Blue = 4,
}
// ColorWithCustomValues.Red === 1
// 同时包含字符串和数字枚举
enum MixedColor {
Red = "red",
Green = 1,
Blue = "blue",
}
// MixedColor.Red === "red"
// 使用枚举成员名称作为字符串
let colorName: string = MixedColor.Red; // 如果myColor是MixedColor.Red,则colorName是'string'
console.log(colorName); // 输出 'red'
// 反向映射:从枚举值获取枚举名(仅限字符串枚举)
let value = MixedColor["Red"] as MixedColor; // 使用as关键字做断言
console.log(value); // 输出 MixedColor.Red
2.2.13 联合类型
联合类型
:| 符号表示联合(Union)操作。当用于类型时,它表示的是“或”的关系,即某个变量的类型可以是几种可能类型之一。
let h: string | number;
h = '张三';
h = 18;
2.2.14 交叉类型
交叉类型
:是将多个类型合并为一个类,使用&
,表示同时,求并集
let j: { name: string } & { age: number };
j = { name: '张三', age: 18 };
交叉类型主要作用是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型
2.2.15 类型别名type
类型别名(type)
:用来给一个类型起个新名字。 仅仅是给类型取了一个新的名字,并不是创建了一个新的类型,类型别名常用于联合类型
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
2.2.16 类型断言
类型断言
:有些情况下,变量的类型对于我们来说是很明确,但TS编译器并不清楚,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:
//类型断言,可以用来告诉编译器变量的实际类型
/*
语法: (变量 as 类型) 或 < 类型 > 变量
*/
let e: unknown;
e='ss' //e实际是字符串,但e的类型设置的是unkonwn
let f: string;
f=e//报错
f = e as string;//告诉ts编译器e就是字符串,这样就不会出现上述f=e报错的情况
// 或
f = <string>e;
3 编译选项
3.1 自动编译文件
编译文件时,使用 -w 指令后,TS编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译。
tsc xxx.ts -w
3.2 自动编译整个项目
一个文件一个文件的编译太麻烦了,我们可以对整个项目进行编译
首先在项目根目录下创建一个ts的配置文件 tsconfig.json
,然后就可以使用tsc
指令,编译项目下的所有ts文件为js文件,当然也可以开启监视模式tsc -w
监视所有的文件
3.3 tsconfig.json的配置选项
tsconfig.json配置总览
{
"compilerOptions": {
"target": "es5", // 指定 ECMAScript 目标版本: 'ES5'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"moduleResolution": "node", // 选择模块解析策略
"experimentalDecorators": true, // 启用实验性的ES装饰器
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
"sourceMap": true, // 把 ts 文件编译成 js 文件的时候,同时生成对应的 map 文件
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"alwaysStrict": true, // 以严格模式检查模块,并在每个文件里加入 'use strict'
"declaration": true, // 生成相应的.d.ts文件
"removeComments": true, // 删除编译后的所有的注释
"noImplicitReturns": true, // 不是函数的所有返回路径都有返回值时报错
"importHelpers": true, // 从 tslib 导入辅助工具函数
"lib": ["es6", "dom"], // 指定要包含在编译中的库文件
"typeRoots": ["node_modules/@types"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"./src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
]
}
3.3.1 include
- 指定需要编译的ts文件目录,是一个数组,其中
*
表示任意文件**
表示任意目录 - 默认值:
["**/*"]
"include":["src/**/*", "test/**/*"] // 所有src目录和test目录下的文件都会被编译
3.3.2 exclude
- 指定不需要被编译的文件目录
- 默认值:
["node_modules", "bower_components", "jspm_packages"]
"exclude": ["./src/hello/**/*"] // src下hello目录下的文件都不会被编译
3.3.3 extends
-
定义被继承的配置文件(和引入外部文件类似)
"extends": "./configs/base" // 自动包含configs目录下base.json中的所有配置信息
3.3.4 files
-
指定被编译文件的列表,只有需要编译的文件少时才会用到(和include类似)
-
列表中的文件都会被TS编译器所编译
"files": [ "core.ts", "sys.ts", "types.ts", "scanner.ts", "parser.ts", "utilities.ts", "binder.ts", "checker.ts", "tsc.ts" ]
3.3.5 compilerOptions
重要的编译选项,在compilerOptions中包含多个子选项,用来完成对编译的配置
target
:设置ts代码编译的目标版本
对于选项有哪些可选值,我们可以随便写一个值,编辑器会提示我们有哪些可选值
"compilerOptions": {
//可选值: “ES3”(默认), “ES5”, “ES6”, “ES2015”, “ES2016”, “ES2017”, “ES2018”, “ES2019”, “ES2020”, “ES2021”, “ESNext”.
"target": "ES6"//ts代码将会被编译为ES6版本的js代码
}
lib
:指定代码运行时所包含的库(宿主环境),一般运行在浏览器环境下的,就不需要自己单独设置这个
"compilerOptions": {
//可选值:“ES5”, “ES6”, “ES2015”...很多
"lib": ["ES6", "DOM"]
}
module
:设置编译后代码使用的模块化系统
"compilerOptions": {
//可选值:“CommonJS”, “AMD”, “System”, “UMD”, “ES6”, “ES2015”, “ES2020”, “ESNext”, “None”, “es2022”, “node12”, “nodenext”
"module": "CommonJS"
}
outDir
:指定编译后文件的所在目录
"compilerOptions": {
"outDir": "./dist"//编译过的js文件将会生成到dist目录。可以将源码与编译后的代码分开存放
}
outFile
:将所有的文件编译为一个js文件
- 默认会将所有的编写在全局作用域中的代码合并为一个js文件,如果 module 制定了 None、System 或 AMD 则会将模块一起合并到文件之中,这种合并由打包工具去做。
"compilerOptions": {
"outFile": "./dist/app.js"
}
rootDir
:指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录
"compilerOptions": {
"rootDir": "./src"
}
3.3.6 其他配置
//是否对js文件编译,默认值:false
"allowJs":false,
//是否对js文件进行语法检查,默认值:false
"checkJs":false
//是否删除注释,默认值:false
"removeComments":false
//不生成编译后的文件,默认值:false
"noEmit":false
//当有错误的时候不生成编译后的文件,默认值:false
"noEmitOnError":false
//是否生成sourceMap,默认值:false
"remove":false
3.3.7 严格检查
//是否启用所有的严格检查(总开关),设置true后相当于开启了所有的严格检查,后面的检查不需要在写了,默认值:false
"strict":false
//是否总是以严格模式对代码进行编译,默认值:false
"alwaysStrict":false
//是否允许隐式的 any 类型,默认值:false
"noImplicitAny":false
//是否允许隐式的 this,默认值:false
"noImplicitThis":false
//严格检查bind、call和apply的参数列表,默认值:false
"strictBindCallApply":false
//严格检查函数的类型,默认值:false
"strictFunctionTypes":false
//检查是否存在空值,默认值:false 可以用a?.b判断a是否是空值
"strictNullChecks":false
//严格检查属性是否初始化,默认值:false
"strictPropertyInitialization":false
3.3.8 额外检查
//是否检查switch语句包含正确的break
"noFallthroughCasesInSwitch":false
//是否检查函数没有隐式的返回值
"noImplicitReturns":false
//是否检查未使用的局部变量
"noUnusedLocals":false
//是否检查未使用的参数
"noUnusedParameters":false
//检查不可达代码;true:忽略不可达代码,false:不可达代码将引起错误
"allowUnreachableCode":false
4 使用Webpack打包TS代码
通常情况下,实际开发中我们都需要使用构建工具对代码进行打包;TS同样也可以结合构建工具一起使用,下边以webpack为例
介绍一下如何结合构建工具使用TS;
4.1 初始化项目
通过执行命令 npm init -y
初始化一个项目并创建package.json文件
使用tsc --init
创建ts的配置文件tsconfig.json
创建src/index.ts
文件,用来编写ts代码
4.2 安装依赖
安装以下依赖包
npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin
-
webpack:构建工具webpack
-
webpack-cli:webpack的命令行工具
-
webpack-dev-server:webpack的开发服务器
-
typescript:ts编译器
-
ts-loader:ts加载器,用于在webpack中编译ts文件
-
html-webpack-plugin:webpack中html插件,用来自动创建html文件
-
clean-wepack-plugin:webpack中的清除插件,每次构建都会先清除目录
4.3 配置webpack
根目录下创建webpack的配置文件webpack.config.js
:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
// 指定入口文件
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"],//以ts js结尾的文件都可以作为模块使用
},
// 配置webpack的loader(打包使用的模块)
module: {
//加载规则
rules: [
{
//规则生效的文件
test: /.ts$/,//以ts结尾的文件
//要使用的loader
use: {
loader: "ts-loader",
},
//排除的文件
exclude: /node_modules/,
},
],
},
// 配置webpack的插件
plugins: [
// 构建前清除目录
new CleanWebpackPlugin(),
//自动创建html文件
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
};
4.3.1 配置TS编译选项
根目录下创建tsconfig.json,配置可以根据自己需要
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"strict": true
}
}
4.3.2 修改package.json配置
修改package.json添加如下配置
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open chrome.exe"
},
...
}
4.4 配置babel
除了webpack,开发中还经常需要结合babel
来对代码进行转换以使其可以兼容到更多的浏览器,通过以下步骤可以将babel引入到项目中。
4.4.1 安装依赖
安装以下依赖包
npm i -D @babel/core @babel/preset-env babel-loader core-js
- @babel/core:babel 的核心工具
- @babel/preset-env:babel 的预定义环境
- babel-loader:babel 在webpack中的加载器
- core-js:core-js 用来使老版本的浏览器支持新版ES语法
4.4.2 修改配置文件
修改webpack.config.js配置文件
这样修改后,使用ts编译后的文件将会再次被babel处理,使得代码可以在大部分浏览器中直接使用,可以在配置选项的targets中指定要兼容的浏览器版本。
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",
},
],
],
},
},
{
loader: "ts-loader",
},
],
exclude: /node_modules/,
},
];
}
4.4.3 项目使用
在src下创建ts文件,并在并命令行执行npm run build
对代码进行编译;
或者执行npm start
来启动开发服务器;
4.5 生产环境和开发环境分别配置
以上是一些基本的配置,但是在实际开发中,webpack在配置开发环境与生产环境时,配置的有些东西不太相同,所以我们应该分开写我们生产环境和开发环境的webpack配置
所以我们就在根目录下创建build文件夹存放我们的webpack配置文件
安装webpack-merge
//基于公共配置通过webpack-merge合并开发或者生产环境的特有配置,生成完整的开发或者生产环境配置
npm i -D webpack-merge
基本配置webpack.base.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
environment: {
arrowFunction: false, // 关闭webpack的箭头函数,可选
},
},
resolve: {
extensions: [".js", ".ts"],
},
module: {
rules: [
{
test: /.ts$/,
use: [
{
loader: "ts-loader",
},
],
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
};
开发环境配置webpack.dev.config.js
module.exports = {
devtool: "inline-source-map",
};
生产环境配置webpack.pro.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [new CleanWebpackPlugin()],
};
配置主文件webpack.config.js
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.config");
const devConfig = require("./webpack.dev.config");
const proConfig = require("./webpack.pro.config");
module.exports = (env, argv) => {
let config = argv.mode === "development" ? devConfig : proConfig;
return merge(baseConfig, config);
};
4.6 编写代码
index.ts
const box = document.querySelector('#app')
const hello: string = 'Hello TS'
if (box !== null) {
box.innerHTML = hello
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TS & webpack</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
4.7 配置命令打包代码
修改package.json添加如下配置
{
......
"scripts": {
"start": "webpack-dev-server --mode=development --config ./build/webpack.config.js",
"build": "webpack --mode=production --config ./build/webpack.config.js"
},
......
}
在src下创建ts文件,并在并命令行执行npm run build
对代码进行编译,执行npm start
来启动开发服务器
5 面向对象
面向对象(OOP Object-Oriented Programming)
是一种优秀的程序设计方法,它的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。
对象
就是一个自包含的实体,用一组可识别的特性(属性)和行为(方法)来标识。程序中的对象就是对现实世界中具体实体的抽象。
一个事物到了程序中就变成了一个对象,在程序中所有的对象都被分成了两个部分:数据和功能,以人为例,人的姓名、性别等属于数据,人可以说话、走路这些属于人的功能。数据在对象中被称为属性,而功能就被称为方法。简而言之,在程序中一切皆是对象。
5.1 类
类
就是具有相同属性和功能的对象的抽象的集合,可以理解为对象的模型。一般是先创建 类
,然后通过实例化 类
来得到 对象
。
定义类
class Cat{
// 属性的类型声明,在TS中必须有初始值,或者在构造函数中初始化
name: string
age: number
// constructor构造函数会在对象创建时调用
constructor(name: string) {
// 初始化属性,this表示当前调用对象
this.name = name
this.age = 3
}
// 方法
sayHi(): void{
// 在方法中可以通过this表示当前调用方法的对象
console.log(`喵,我是 ${this.name}`)
}
}
使用类
let Tom = new Cat("Tom") // 创建Cat类的实例
console.log(Tom) // Cat: { "name": "Tom", "age": 3 } (实例上只有属性)
Tom.say() // "喵,我是Tom" (方法在原型上,所以调用可以通过原型链访问到)
5.2 继承
继承(Inheritance
):在继承机制下形成有层级的类,使得低层级的类可以延用高层级类的特征和方法。
继承的实现方式:
实现继承
:直接使用基类公开的属性和方法,无需额外编码。接口继承
:仅使用接口公开的属性和方法名称,需要子类实现。
继承的作用:
-
复用代码,减少类的冗余代码。
-
使得类与类之间产生关系,为多态的实现打下基础。
创建一个猫类的父类 Animal
class Animal{
name: string
age: number
construcor(name: string){
this.name = name
this.age = 3
}
say(){
console.log('...')
}
}
使用 extends
关键字来继承 Animal
类,在子类中使用 super
完成对父类的引用
class Cat extends Animal {
color: string
construcor(name: string, color: string){
super(name)//调用父类的构造函数
this.color = color
}
}
通过继承可以将其他类中的属性和方法引入到当前类中 (在不修改类的情况下完成对类的扩展)
class 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();
发生继承时,如果子类中的方法会替换掉父类中的同名方法,这就称为方法的重写
class Animal{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
run(){
console.log(`父类中的run方法!`);
}
}
class Dog extends Animal{
bark(){
console.log(`${this.name}在汪汪叫!`);
}
// 重写父类的run方法
run(){
console.log(`子类中的run方法,会重写父类中的run方法!`);
}
}
const dog = new Dog('旺财', 4);
dog.bark();
5.3 抽象
抽象类(abstract class
)是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例
abstract class Animal {
eat() {
console.log('eat')
}
abstract sleep(): void
}
不能直接实例化抽象类,通常需要我们创建子类继承基类,然后可以实例化子类
class Dog extends Animal {
name: string
constructor(name: string){
super()
this.name = name
}
run(){}
sleep(){
console.log('dog sleep')
}
}
抽象类中可以添加抽象方法,使用abstract
开头的方法叫做抽象方法,抽象方法没有方法体只能定义在抽象类中,继承抽象类时抽象方法必须要实现
abstract class Animal{
abstract run(): void; // 抽象方法
bark() {
console.log('动物在叫~');
}
}
class Dog extends Animals{
run() {
console.log('狗在跑~');
}
}
5.4 接口
接口(Interface)
是对行为的抽象,而具体如何行动需要由类(class)去实现(implement)
接口
主要负责定义一个类的结构,可以在定义类的时候去限制类的结构,其中所有的属性都不能有实际的值,所有的方法都是抽象方法。接口可以去限制一个对象的接口,对象只有包含接口中定义的所有属性和方法时才能匹配接口。
接口一般首字母大写。
类型别名(type)
是给类型起一个新名字,当作一个限制器使用 类似于接口,支持基本类型、联合类型、元组,给一个类型重新定义一个新的名字,常用于联合类型
示例(检查对象类型)
// 描述一个对象的类型,类型别名type
type MyType = {
name: string,
age: number
};
type numstring = test|string; // 联合类型
let numstr:numstring = 32;
// 接口用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法,可以当成类型声明去使用
interface MyInterface {
name: string;
age: number;
}
//接口可以重复声明,相当于一个接口中有name age gender三个属性
interface MyInterface {
gender: string;
}
const obj: MyInterface = {
name: 'sss',
age: 111,
gender: '男'
};
示例(实现)
// 接口可以在定义类的时候去限制类的结构,其中所有的属性都不能有实际的值,所有的方法都是抽象方法
interface MyInter {
name: string;
sayHello(): void;
}
//定义类时,可以使类去实现一个接口,使类满足接口的要求
class MyClass implements MyInter {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
console.log('大家好~~');
}
}
5.5 封装
对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装
,默认情况下,对象的属性是可以任意的修改的,为了确保数据的安全性,在TS中可以对属性的权限进行设置
封装的作用:
- 可隐藏实体实现的细节
- 提高安全性,设定访问控制,只允许具有特定权限的使用者调用
- 简化编程,调用方无需知道功能是怎么实现的,即可调用
5.5.1 静态属性和只读属性
静态属性(static)
:也称为类属性。使用静态属性无需创建实例,通过类即可直接使用,静态属性使用 static
开头
只读属性(readonly)
:如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改
示例
class Tools{
readonly a =10
static PI = 3.1415926;
}
const tool = new Tools();
tool.a//10 只读属性不可以修改
Tools.PI//静态属性可以直接获取,3.1415926
5.5.2 属性修饰符
TS中属性具有三种修饰符:
public(默认值)
:修饰的属性可以在任意位置修改protected (受保护成员)
,可以在类或子类的内部修改private (私有成员)
:修饰的属只能在类的内部修改
public修饰符示例
class Person{
// public可以不写
public name: string;
age: number;
constructor(name: string, age: number){
this.name = name; // 可以在类中修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中可以修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 可以通过对象修改
protected修饰符示例
class Person{
protected name: string;
protected age: number;
constructor(name: string, age: number){
this.name = name; // 可以修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中可以修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改
private修饰符示例
class Person{
private name: string;
private age: number;
constructor(name: string, age: number){
this.name = name; // 可以修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中不能修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改
5.5.3 属性存取器
对于一些不希望被任意修改的属性,可以将其设置为private
,直接将其设置为private
将导致无法再通过对象修改其中的属性。
那么我们可以在类中定义一组读取、设置属性的方法使得私有属性private可以被外部访问,这种对属性读取或设置的属性被称为属性存取器
读取属性的方法叫做setter
方法,设置属性的方法叫做getter
方法。
class Person{
private _name: string;
constructor(name: string){
this._name = name;
}
get name(){
return this._name;
}
set name(name: string){
this._name = name;
}
}
const p1 = new Person('孙悟空');
console.log(p1.name); // 通过getter读取name属性
p1.name = '猪八戒'; // 通过setter修改name属性
5.5.4 任意属性
有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用索引签名
的形式来满足上述要求。
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。
一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {
name: string;
age?: number; // 这里真实的类型应该为:number | undefined
[propName: string]: string | number | undefined;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
5.6 泛型
5.6.1 泛型介绍
定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型
就可以表示这个类型。
举个例子
function test(arg: any): any{
return arg;
}
test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的,由于类型不确定所以参数和返回值均使用了any,但是很明显这样做是不合适的,首先使用any会关闭TS的类型检查,其次这样设置也不能体现出参数和返回值是相同的类型
使用泛型
function test<T>(arg: T): T{
return arg;
}
这里的<T>
就是泛型
,T 是一个抽象类型,只有在调用的时候才确定它的值。泛型就是表示某个类型。
T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
如何使用上面的函数?
方式一(直接使用):
//使用时可以直接传递参数使用,类型会由TS自动推断出来,但有时编译器无法自动推断时还需要使用下面的方式
test(10)
方式二(指定类型):
//指定类型
test<number>(10)
可以同时指定多个泛型,泛型间使用逗号隔开:
function test<T, K>(a: T, b: K): K{
return b;
}
test<number, string>(10, "hello");
使用泛型时,完全可以将泛型当成是一个普通的类去使用,类中同样可以使用泛型
class MyClass<T>{
prop: T;
constructor(prop: T){
this.prop = prop;
}
}
除此之外,也可以对泛型的范围进行约束
interface MyInter{
length: number;
}
//使用T extends MyInter表示泛型T必须是MyInter的子类,不一定非要使用接口类和抽象类同样适用。
function test<T extends MyInter>(arg: T): number{
return arg.length;
}
React.FC表示函数式组件,是在TypeScript中使用的一个泛型。
interface PropsType{
msg: string;
}
const app: React.FC<PropsType> = ({msg}) => {
return(
<h1>hello,{msg}</h1>
)
5.6.2 typeof
typeof` 的主要用途是在类型上下文中获取变量或者属性的类型
interface Person {
name: string;
age: number;
}
const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person
const lolo: Sem = { name: "lolo", age: 5 }
在上面代码中,我们通过 typeof 操作符获取 sem 变量的类型并赋值给 Sem 类型变量,之后我们就可以使用 Sem 类型
你也可以对嵌套对象执行相同的操作:
const Message = {
name: "jimmy",
age: 18,
address: {
province: '四川',
city: '成都'
}
}
type message = typeof Message;
/*
type message = {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
*/
5.6.3 Omit
Omit
用来创建从原始类型中移除了某些属性的新类型
Omit <T, K>
其中 T 是 type也就是类型的简写, K 是 key 的简写,所以这里的意思就是忽略该类型的key属性
import { FilterDropdownProps } from 'antd/es/table'
type TagProps = {
slug: string
name: string
}
type MyFilterDropdownProps = Omit<FilterDropdownProps, 'selectedKeys' | 'setSelectedKeys'> & {
selectedKeys: TagProps
setSelectedKeys: (selectedKeys: TagProps[]) => void
}
**‘selectedKeys’ | ‘setSelectedKeys’ 是一个联合类型,表示的是这两个字串字面量类型的组合,表示要排除的 FilterDropdownProps 类型中的属性键名,这些键名可能是 ‘selectedKeys’、‘setSelectedKeys’ **
这里的意思就是要忽略 FilterDropdownProps 的 selectedKeys 和 setSelectedKeys 属性,然后我们还做了一步最重要的操作,就是通过 & 符号将我们自己定义的 selectedKeys 和 setSelectedKeys 和 FilterDropdownProps 剩下的属性组合起来了,最后再赋值给 MyFilterDropdownProps 类型。
5.6.4 Record
Record的内部定义,接收两个泛型参数;Record后面的泛型就是对象键和值的类型
作用 :定义义一个对象的 key 和 value 类型
Record<key type, value type>
Record<string, never> 空对象
Record<string, unknown> 任意对象
{} 任何不为空的对象
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
}
泛型K即为第一次参数
in的意思就是遍历,如上就是将 类型string进行遍历,也就是string
每个属性都是传入的T类型,如 string: PersonModel
举例
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInfo> = {
about: { title: "about" },
contact: { title: "contact" },
home: { title: "home" },
};
6 编写TS 代码的一些建议
6.1 尽量减少重复代码
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate {
firstName: string;
lastName: string;
birth: Date;
}
PersonWithBirthDate 接口比 Person 接口只是多了一个 birth 属性,其他的属性一致,接口的扩展就是继承,通过 extends
来实现,如果是类型别名的扩展可以使用使用交叉运算符(&)
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
type PersonWithBirthDate = Person & { birth: Date };
另外,有时候你可能还会发现自己想要定义一个类型来匹配一个初始配置对象的「形状」,可以使用 typeof 操作符来快速获取配置对象的「形状」
const INIT_OPTIONS = {
width: 640,
height: 480,
color: "#00FF00",
label: "VGA",
};
interface Options {
width: number;
height: number;
color: string;
label: string;
}
type Options = typeof INIT_OPTIONS;
6.2 使用更精确的类型替代字符串类型
假设你正在构建一个音乐集,并希望为专辑定义一个类型。这时你可以使用 interface 关键字来定义一个 Album 类型:
interface Album {
artist: string; // 艺术家
title: string; // 专辑标题
releaseDate: string; // 发行日期:YYYY-MM-DD
recordingType: string; // 录制类型:"live" 或 "studio"
}
对于 Album 类型,你希望 releaseDate 属性值的格式为 YYYY-MM-DD,而 recordingType 属性值的范围为 live 或 studio。但因为接口中 releaseDate 和 recordingType 属性的类型都是字符串,所以在使用 Album 接口时,可能会出现以下问题:
const dangerous: Album = {
artist: "Michael Jackson",
title: "Dangerous",
releaseDate: "November 31, 1991", // 与预期格式不匹配
recordingType: "Studio", // 与预期格式不匹配
};
虽然 releaseDate 和 recordingType 的值与预期的格式不匹配,但此时 TypeScript 编译器并不能发现该问题。为了解决这个问题,你应该为 releaseDate 和 recordingType 属性定义更精确的类型,比如这样:
interface Album {
artist: string; // 艺术家
title: string; // 专辑标题
releaseDate: Date; // 发行日期:YYYY-MM-DD
recordingType: "studio" | "live"; // 录制类型:"live" 或 "studio"
}
重新定义 Album 接口之后,对于前面的赋值语句,TypeScript 编译器就会提示以下异常信息:
const dangerous: Album = {
artist: "Michael Jackson",
title: "Dangerous",
// 不能将类型“string”分配给类型“Date”。ts(2322)
releaseDate: "November 31, 1991", // Error
// 不能将类型“"Studio"”分配给类型“"studio" | "live"”。ts(2322)\
recordingType: "Studio", // Error
};
为了解决上面的问题,你需要为 releaseDate 和 recordingType 属性设置正确的类型,比如这样:
const dangerous: Album = {
artist: "Michael Jackson",
title: "Dangerous",
releaseDate: new Date("1991-11-31"),
recordingType: "studio",
};