1. TypeScript概述
1-1. 为什么要学习TypeScript
- 就业 或 获得更大的竞争优势
- 获得更好的开发体验
- 解决JS中一些难以处理问题
1-2. JS开发中的问题
- 使用了不存在的变量、函数或成员
- 把一个不确定的类型当作一个确定的类型处理
- 在使用null或undefined的成员
js的原罪:
- js语言本身的特性,决定了该语言无法适应大型的复杂的项目
- 弱类型:某个变量,可以随时更换类型。
- 解释性:错误发生的时间,是在运行时
前端开发中,大部分的时间都是在排错
1-3. TypeScript
简称TS
TypeScript是JS的超集,是一个可选的、静态的类型系统
- 超集
整数、正整数, 整数是正整数的超集
- 类型系统
对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查 - 可选的
学习曲线非常平滑。 - 静态的(在运行之前)
无论是浏览器环境,还是node环境,无法直接识别ts代码
babel: es6 -> es5
tsc: ts -> es
tsc: ts编译器
静态:类型检查发生的时间,在编译的时候,而非运行时
TS不参与任何运行时的类型检查。
TS的常识:
- 2012年微软发布 (ES6,ES2015)
- Anders Hejlsberg 负责开发TS项目
- 开源、拥抱ES标准
- 版本3.4
- 官网:http://www.typescriptlang.org/
中文网:https://www.tslang.cn/ 个人翻译
额外的惊喜:
有了类型检查,增强了面向对象的开发
js中也有类和对象,js支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题。
使用TS后,可以编写出完善的面向对象代码。
2. 在node中搭建TS开发环境
2-1. 安装TypeScript
推荐全局安装
1 | npm install -g typescript |
使用:
1 | #编译指定文件文件 |
默认情况下,TS会做出下面几种假设:
- 假设当前的执行环境是dom(浏览器环境)
- 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行
- 编译的目标代码是ES3
有两种方式更改以上假设:
使用tsc命令行的时候,加上选项参数
1
2文件改变自动执行编译
tsc --watch使用ts配置文件,更改编译选项
1
2使用了配置文件后,使用tsc进行编译时,不能跟上文件名
tsc
2-2. TS的配置文件
创建配置文件方法:
- 手动创建
tsconfig.json
文件 - 通过命令创建:
tsc --init
使用了配置文件后,使用tsc进行编译时,不能跟上文件名,如果跟上文件名,会忽略配置文件。
1 | // tsconfig.json |
使用第三方库支持node环境:
1 | npm i -D @types/node |
@types是一个ts官方的类型库,其中包含了很多对js代码的类型描述。
JQuery:用js写的,没有类型检查
安装@types/jquery,为jquery库添加类型定义
2-3. 使用第三方库简化流程
2-3-1. ts-node
将ts代码在内存中完成编译,同时完成运行
编译并运行,但是不会生成编译后文件
且修改后代码后需重新执行命令
安装
1
npm install -g ts-node
使用
1
2指定入口文件,这里可以指定文件名称
ts-node src/index.ts
2-3-2. nodemon
用于检测文件的变化
安装
1
npm install -g nodemon
使用
- 检测所有文件
1
2
3
4nodemon 检测文件变化
--exec 执行 xx 命令
现在监测的是所有文件,配置文件修改也会重新编译
nodemon --exec ts-node src/index.ts- 只检测ts文件
1
2-e ts 监控文件的扩展名为ts
nodemon -e ts --exec ts-node src/index.ts- 只检测src文件夹下的ts文件
1
2--watch src 指定监听文件夹
nodemon --watch src -e ts --exec ts-node src/index.ts添加到脚本使用
1
2
3
4
5
6
7
8
9{
"scripts": {
"dev": "nodemon --watch src -e ts --exec ts-node src/index.ts",
"build": "rd /s /q dist & tsc"
},
"devDependencies": {
"@types/node": "^20.4.5"
}
}rd /s /q dist & tsc
说明:
先运行rd /s /q dist
在运行tsc
rd 这是”remove directory”的缩写,表示删除目录(windows)
/s 这个参数表示递归删除目录及其所有子目录和文件
/q 这个参数表示”quiet”,也就是在删除时不显示确认提示。mac中可以使用
rm -rf dist
指令
3. 基本类型约束
TS是一个可选的静态的类型系统
3-1. 类型约束和编译结果对比
3-1-1. 如何进行类型约束
仅需要在 变量、函数的参数、函数的返回值位置加上
:类型
1
2
3
4
5let name: string;
function add(a:number, b:number):number{
return a + b;
}ts在很多场景中可以完成类型推导
1
2
3
4
5
6
7
8
9
10
11
12let name: string;
//可以推导出add返回是为number
function add(a:number, b:number){
return a + b;
}
//可以推导出num的类型为number
let num = add(3, 4)
//可以推导出name为string类型;后续无法赋值为其他类型
let name = "sss"any: 表示任意类型,对该类型,ts不进行类型检查
1
2
3
4let age; //推导不出则为any类型,any类型ts不进行检查
age = 23;
age = "23";
小技巧,如何区分数字字符串和数字,关键看怎么读?
如果按照数字的方式朗读,则为数字;否则,为字符串。
3-1-2. 源代码和编译结果的差异
编译结果中没有类型约束信息
1 | // let name: string; |
1 | // let name: string; |
3-2. 基本类型
number: 数字
1
let age: number = 18;
string: 字符串
1
let name: string='luckystar';
boolean: 布尔
1
let married: boolean=false;
数组
1
2
3let arr1: number[]; //【方式一】直接指明类型
let arr3: Array<number> //【方式二】语法糖
let arr2 = [1,4,5]; //使用类型推导object: 对象
null 和 undefined
null和undefined是所有其他类型的子类型,它们可以赋值给其他类型
通过添加strictNullChecks:true
,可以获得更严格的空类型检查,null和undefined只能赋值给自身。
3-3. 其他常用类型
联合类型:多种类型任选其一
1
let name: string | undefined;
配合类型保护进行判断
类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof可以触发类型保护。
1
2
3if(typeof name === 'string'){
//类型保护
}void类型:通常用于约束函数的返回值,表示该函数没有任何返回
1
2
3function printMenu(): void {
//...
}never类型:通常用于约束函数的返回值,表示该函数永远不可能结束
1
2
3
4
5
6
7
8
9function throwError(msg: string): never {
throw new Error(msg);
}
function alwaysDoSomething(): never {
while (true) {
//...
}
}字面量类型:使用一个值进行约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 基本类型约束,不可赋值为其他内容
let gender: "男" | "女";
gender = "女";
gender = "男";
//对象约束,必须存在约束属性,且类型符合约束条件
let user: {
name:string
age:number
}
user = {
name:"34",
age:33
}元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定
1
2
3
4// tu数组为两项,并且第一项为string,第二项为number
let tu: [string, number];
tu = ["3", 4];any类型: any类型可以绕过类型检查,因此,any类型的数据可以赋值给任意类型
1
2
3let data:any = "luckystar";
let num:number = data;
4. 类型别名
对已知的一些类型定义名称
1 | type 类型名 = ... |
代码示例:
1 | // 定义类型别名 |
5. 函数的相关约束
5-1. 函数重载
函数重载:在函数实现之前,对函数调用的多种情况进行声明
1 | // 函数的重载;规定combine函数的参数情况只能同时为number或者string |
5-2. 可选参数
可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。
1 | // 参数c为可选参数 |
5-3. 默认参数
1 | // 默认参数为可选参数 |
6. 扩展类型-枚举
扩展类型:类型别名、枚举、接口、类
枚举通常用于约束某个变量的取值范围。
字面量和联合类型配合使用,也可以达到同样的目标。
6-1. 字面量类型的问题
在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。
1
2
3
4
5
6let gender: "男" | "女";
gender = "女";
gender = "男";
function getUsers(g:"男" | "女"):User[] {
return [];
}逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
1
2
3
4
5
6
7
8
9
10
11// 若果需要修改Gender的字面量类型会导致后面使用全部都需要修改
// 需求更改:帅哥 美女
type Gender = "男" | "女"
let gender: Gender;
gender = "女";
gender = "男";
function getUsers(g:Gender):User[] {
return [];
}
//...字面量类型不会进入到编译结果。
6-2. 枚举
如何定义一个枚举:
1 | enum 枚举名{ |
代码示例:
1 | //可以修改实际值,不会影响后续代码,而且枚举会出现在编译结果中,编译结果中表现为对象。 |
枚举会出现在编译结果中,编译结果中表现为对象。
枚举的规则:
枚举的字段值可以是字符串或数字
数字枚举的值会自动自增(默认从零开始)
被数字枚举约束的变量,可以直接赋值为数字
1
2
3
4
5
6
7
8
9
10
11
12
13
14//默认 0 1 2
enum Level {
level1,
level2,
level3
}
//可以赋值,尽量不要这样做
// let str1: Level = 1;
// str1 = 2;
//正确做法
let str2: Level = Level.level1;
str2 = Level.level2;数字枚举的编译结果 和 字符串枚举有差异
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//数字枚举结果
var Level;
(function (Level) {
Level[Level["level1"] = 0] = "level1";
Level[Level["level2"] = 1] = "level2";
Level[Level["level3"] = 2] = "level3";
})(Level || (Level = {}));
/*
{
level1:0,
level2:1,
level3:2,
0:"level1",
1:"level2",
2:"level3"
}
*/
最佳实践:
- 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
- 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值
6-3. 扩展知识:位枚举(枚举的位运算)
针对的数字枚举
位运算:两个数字换算成二进制后进行的运算
代码示例: 实现权限的修改和判断
1 | //定义权限 |
7. 模块化
本节课相关配置:
配置名称 | 含义 |
---|---|
module | 设置编译结果中使用的模块化标准 |
moduleResolution | 设置解析模块的模式 |
noImplicitUseStrict | 编译结果中不包含”use strict” |
removeComments | 编译结果移除注释 |
noEmitOnError | 错误时不生成编译结果 |
esModuleInterop | 启用es模块化交互非es模块导出 |
前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext
TS中如何书写模块化语句?
编译结果是什么?
7-1. TS中如何书写模块化语句
TS中,导入和导出模块,统一使用ES6的模块化标准
7-2. 编译结果中的模块化
可配置
TS中的模块化在编译结果中:
- 如果编译结果的模块化标准是ES6: 没有区别
- 如果编译结果的模块化标准是commonjs:导出的声明会变成exports的属性,默认的导出会变成exports的default属性;
7-3. 解决默认导入的错误
1 | //编译为 commonjs 会报错,因为commonjs 是没有默认导出的 |
7-4. 如何在TS中书写commonjs模块化代码
开启
esModuleInterop
配置
或者采用下方写法(不推荐):
导出:export = xxx
导入:import xxx = require(“xxx”)
代码示例:
不推荐 这总写法无法推导出类型
1 | // index.js |
ts推荐写法
1 | // index.js |
7-5. 模块解析
模块解析:应该从什么位置寻找模块
TS中,有两种模块解析策略
- classic:经典
- node:node解析策略(唯一的变化,是将js替换为ts)
- 相对路径
require("./xxx")
- 非相对模块
require("xxx")
- 相对路径
8. 接口和类型兼容性
扩展类型-接口
8-1. 接口的概念
接口:inteface
扩展类型:类型别名、枚举、接口、类
TypeScript的接口:用于约束类、对象、函数的契约(标准)
契约(标准)的形式:
- API文档,弱标准
- 代码约束,强标准
8-2. 接口的使用
和类型别名一样,接口,不出现在编译结果中
接口约束对象
1
2
3
4
5
6
7
8
9
10//在约束对象时,接口(interface)和类型别名(type)无太大区别
interface User {
name: string
age:number
}
let u: User = {
name: "luckystar",
age: 18
}接口约束函数
对象中的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface User {
name: string
age:number
// 约束函数:无参数,无返回值;注意不能写函数实现(两种写法都可以)
// sayHello: () => void
sayHello():void
}
let u: User = {
name: "luckystar",
age: 18,
sayHello(){
console.log("sayHello");
}
}普通函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 在约束函数时,接口(interface)和类型别名(type)无太大区别
// type Condition = (n:number) => boolean
//接口约束函数写法
interface Condition {
(n:number):boolean;
}
function sum (numbers:number[], callBack: Condition) {
let s = 0;
numbers.forEach(n => {
if(callBack(n)){
s += n;
}
})
return s;
}
const result = sum([3,4,5,7,11], n=>n%2 !== 0);
console.log(result);当使用类型别名约束方法时也可以如下书写
1
2
3
4//当花括号中只有方法约束时,花括号相当于定界符;并不是对象
type Condition = { //定界符
(n:number):boolean;
}
8-3 接口的继承
可以通过接口之间的继承,实现多种接口的组合
1 | interface A { |
使用类型别名可以实现类似的组合效果,需要通过&
,它叫做交叉类型
1 | //交叉类型 |
它们的区别:
- 子接口不能覆盖父接口的成员
- 交叉类型会把相同成员的类型进行交叉
8-4. readonly 修饰符
只读修饰符,修饰的目标是只读
1 | //修饰变量 |
注意: 只读修饰符不在编译结果中
8-5. 类型兼容性
B->A,如果能完成赋值,则B和A类型兼容
鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可
基本类型:完全匹配
对象类型:鸭子辨型法
类型断言
1 | interface Duck { |
当直接使用对象字面量赋值的时候,会进行更加严格的判断
1 | interface Duck { |
- 函数类型
一切无比自然
参数:传递给目标函数的参数可以少,但不可以多
返回值:要求返回必须返回;不要求返回,你随意;
9. TS中的类
面向对象思想
基础部分,学习类的时候,仅讨论新增的语法部分。
9-1. 属性
使用属性列表来描述类中的属性
1 | //typescript |
编译后:
1 | //javascript |
9-1-1. 属性的初始化检查
在配置文件tsconfig.json
中添加如下配置,当类中的属性在构造函数中没有赋值时会提示strictPropertyInitialization:true
9-1-2. 属性的初始化位置
构造函数中
1
2
3
4
5
6
7
8
9
10
11class User {
name: string
age: number
gender: "男" | "女"
constructor(name: string, age: number, gender:"男" | "女" = "男"){
this.name = name;
this.age = age;
this.gender = gender;
}
}
const u = new User("luckystar", 18);属性默认值
1
2
3
4
5
6
7
8
9
10class User {
name: string
age: number
gender: "男" | "女" = "男"
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
const u = new User("luckystar", 18);
9-1-3. 属性可以修饰为可选的
1 | class User { |
9-1-4. 属性可以修饰为只读的
1 | class User { |
9-1-5. 使用访问修饰符
访问修饰符可以控制类中的某个成员的访问权限
- public:默认的访问修饰符,公开的,所有的代码均可访问
- private:私有的,只有在类中可以访问
- protected:暂时不讲
访问修饰符可以修饰属性也可以修饰方法
1 | class User { |
9-1-6. 属性简写
如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写
1 | class User { |
9-1-7. 访问器
作用:用于控制属性的读取和赋值
1 | class User { |
10. 泛型
有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)
泛型:是指附属于函数、类、接口、类型别名之上的类型
泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型
很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型
如果无法完成推导,并且又没有传递具体的类型,默认为空对象
泛型可以设置默认值
10-1. 在函数中使用泛型
在函数名之后写上<泛型名称>
1 | //声明类型变量T;定义函数时无法确定类型,只有调用时才能确定 |
10-2. 如何在类型别名、接口、类中使用泛型
直接在名称后写上<泛型名称>
类型别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//回调函数:判断数组中的某一项是否满足条件
// type callback = (n: number, i: number) => boolean;
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 arr = [1,2,3,4,5];
//这里TS 是可以推断出类型的
console.log(filter(arr, n => n % 2 !== 0));接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16interface 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 arr = [1,2,3,4,5];
console.log(filter(arr, n => n % 2 !== 0));类
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
29export class ArrayHelper<T> {
constructor(private arr: T[]) { }
take(n: number): T[] {
if (n >= this.arr.length) {
return this.arr;
}
const newArr: T[] = [];
for (let i = 0; i < n; i++) {
newArr.push(this.arr[i]);
}
return newArr;
}
shuffle() {
for (let i = 0; i < this.arr.length; i++) {
const targetIndex = this.getRandom(0, this.arr.length);
const temp = this.arr[i];
this.arr[i] = this.arr[targetIndex];
this.arr[targetIndex] = temp;
}
}
private getRandom(min: number, max: number) {
const dec = max - min;
return Math.floor(Math.random() * dec + max);
}
}
10-3. 泛型约束
泛型约束,用于现实泛型的取值
1 | interface hasNameProperty { |
10-4. 多泛型
代码示例:
1 | //将两个数组进行混合 |
__END__