为什么要学习TypeScript
- 就业 或者 获得更大的竞争优势
- 获得更好的开发体验
- 解决js中一些难以处理的问题
JS开发中的问题
- 使用了不存在的变量、函数或成员
- 把一个不确定的类型当成一个确定的类型来进行处理
- 在使用null或undefined的成员
js的原罪
- js语言本身的特性,决定了该语言无法适应大型的复杂的项目
- 弱类型:某个变量,可以随时更换类型
- 解释性:错误发生的时间,是在运行时
前端开发中,大部分的时间都是在排错
TypeScript
简称TS
TypeScript是JS的超集,是一个可选的、静态的类型系统
超集
整数、正整数,整数是正整数的超集类型系统
对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查可选的
学习曲线非常平滑,即ts中新增的内容在开发过程中可用可不用静态的
无论是浏览器环境,还是node环境,无法直接识别ts代码babel:es6 -> es5
tsc:ts -> js
tsc:ts编译器
静态:类型检查发生的时间,在编译的时候,而非运行时
TS不参与任何运行时的类型检查
TS的常识
额外的惊喜
有了类型的检查,增强了面向对象给的开发
js中也有类和对象,js支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题。
使用TS后,可以编写出完善的面向对象代码
node环境搭建ts开发环境
安装TypeScript
建议全局安装
npm install -g typescript
编译ts文件,在编译完成后,当前目录下会生成一个同名的js文件,即为ts代码的编译结果
tsc xxx.ts
默认情况下,TS会做出下面几种假设
- 假设当前的执行环境是dom
- 如果代码中没有使用模块化语句(import、export),便认为改代码是全几乎执行的
- 编译的目标代码是ES3
有两种方式更改以上假设:
- 使用tsc命令行的时候,加上选项参数
- 使用ts配置文件,更改编译选项
TS的配置文件
通过tsc --init
可自动生成ts配置文件,一下为一些简单配置
{
"compilerOptions": {
"target": "es2016",//配置编译目标代码的版本标准
"module": "CommonJS",//配置编译目标使用的模块化标准 commonjs或es6
"lib": ["es2016"],//配置代码编写时的宿主环境,node需要通过安装@types/node来进行额外配置,一般情况下不会配置dom环境
"outDir": "./dist"//指定编译结果存放目录
},
"include": ["./src"]//指定编译目录
// "files": ["./src/index.ts"]//指定编译目标
}
使用了配置文件后,使用tsc进行编译时,不能跟上文件名,如果跟上文件名,会忽略文件名,使用方法,直接tsc
运行
@types/node
@types是一个ts官方的类型库,其中包含了很多对js代码的类型描述
JQuery:用js写的,没有类型检查
安装@types/jquery,为jquery库添加类型定义
使用第三方库简化流程
ts-node:可以将ts代码在内存中完成编译,同时完成运行
nodemon:用于监测文件的变化nodemon --exec ts-node <目标文件>
可通过在package.json中加入一下命令来简化流程
"scripts": {
"dev": "nodemon --watch src -e ts --exec ts-node src/index.ts"
}
在开发完成后通过tsc
命令来完成最终的编译,生成编译结果
基本类型检查
基本类型约束
TS是一个可选的静态的类型系统
如何进行类型约束
仅需要在变量、函数的参数、函数的返回值后加上:类型
即可
let name: string;
function sum(a: number, b: string): number|string{
return a + b;
}
let name: any;
ts在很多场景中可以完成类型推导,比如在为一个未约束类型的变量赋值时,赋值的类型就会对该变量产生约束,推导出来的类型与手动约束的类型作用完全一致
any:表示任意类型,对该类型,ts不进行类型检查
小技巧,如何区分数字字符串和数字,关键看怎么读,比如手机号、身份证号之类
如果按照数字的方式朗读,则为数字;否则,为字符串
源代码和编译结果的差异
编译结果中没有类型约束的信息
基本类型
- number:数字
- string:字符串
- boolean:布尔值
- number[]:数字类型数组,还可写为
Array<number>
- string[]:字符串类型数组
Array<string>
,还可以是let arr:(number|string)[]
- object:对象
- null和undefined
null和undefined时所有其他类型的子类型,即它可以赋值给其他类型
可以在tsconfig.json中的compilerOptions
下加入"strictNullChecks": true
来进行约束,从而获得更严格的空类型检查,从此null和undefined只能赋值给自身
其他类型
- 联合类型,多种类型任选其一,通过
|
来连接两种类型,使变量可以约束为多种类型
可配合类型保护进行判断,类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型 ```tsx let name: string|undefined; if(typeof name === “string”){
}
-
void类型:通常用于约束函数的返回值,表示该函数没有任何返回值
```tsx
function pringMenu():void{
console.log("登录")
}
- never类型:通常用于约束函数的返回值,表示该函数永远不可能结束
function throwError(msg:string): never{ throw new Error(msg); }
- 字面量类型:使用一个值进行约束(一般不会使用该种方式)
let a: "A"; let gender:"男"|"女" = "男"; let arr: [];//arr永远只能取值为一个空数组 let user: { name: string, age: number }
- 元组类型(Tuple):一个固定长度的数组,并且数组中每一项的类型确定
let tu: [string, number];//改数组只能有两项,且第一项必须为字符串,第二项必须为数字
- any类型:any类型的变量
let data: any = "aefdewa"; let num: number = data; console.log(num); //any类型可能会出现一些隐患,比如上述代码
类型别名
对已知的一些类型定义名称
type 类型名 = 自定义类型
type Gender = "男"|"女";
type User = {
name: string,
age: number,
gender: Gender
}
let u:User = {
name: "wjn",
age: 25,
gender: "男"
}
function getUsers(g:Gender):User[]{
return [];
}
函数的相关约束
- 函数重载,在函数调用之前,对函数进行多重声明
```tsx
/**
- 得到a和b的乘积
- @param a
- @param b / function combine(a:number, b:number):number; /*
- 得到a和b的拼接
- @param a
- @param b
*/
function combine(a:string, b:string):string;
function combine(a:number|string, b:number|string):number|string{
if(typeof a === “number” && typeof b === “number”){
}else if(typeof a === “string” && typeof b === “string”){return a * b;
} throw new Error(“a和b必须使相同的类型”); }return a + b;
const result = combine(2, 3); const result1 = combine(“3”, “3”)
-
可选参数,可以在某些参数名后加上问好,表示改参数可以不用传递。可选参数必须在参数列表的末尾
```tsx
function sun(a:number, b:string, c?:number){
if(c){
return a + b + c;
}else{
return a + b;
}
}
扑克牌小练习
- 目标:创建一副扑克牌(不包含大小王),打印该扑克牌
```tsx
type Deck = NormalCard[];
type Color = “♥”|”♠”|”♦”|”♣”
type NormalCard = {
color:Color,
mark:number
}//对一张扑克牌的约束
function createDeck():Deck{
const deck: Deck = [];
for(let i = 1; i <= 13; i++){
} return deck; }deck.push({ mark: i, color: "♠" }); deck.push({ mark: i, color: "♥" }); deck.push({ mark: i, color: "♦" }); deck.push({ mark: i, color: "♣" });
function printDeck(deck: Deck){ let result = “\n”; deck.forEach((card, i) => { let str = card.color; if(card.mark <= 10){ str += card.mark; }else if(card.mark === 11){ str += “J”; }else if(card.mark === 12){ str += “Q”; }else{ str += “K” } result += str + “\t”; if((i + 1) % 6 === 0){ result += “\n”; } }) console.log(result);
}
const deck = createDeck(); printDeck(deck);
<a name="606eff09"></a>
# 扩展类型-枚举
> 扩展类型:类型别名、枚举、接口、类
枚举通常用于约束某个变量的取值范围
字面量和联合类型配合使用,也可以达到同样的目标
<a name="e83b30bd"></a>
## 字面量类型的问题
- 在类型约束的位置,会产生重复的代码
- 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改
- 字面量类型不会进入到编译结果
<a name="56975625"></a>
### 枚举
如何定义一个枚举:
```tsx
enum 枚举名{
枚举字段1 = 值1,
枚举字段2 = 值2
}
在进行赋值时,通过枚举字段来进行赋值,从而可避免逻辑含义和真实的值产生混淆,避免后续维护过程中产生大量的修改
枚举会出现在编译结果中编译结果中表现为对象 ```tsx //编译前 enum Gender{ male = “男”, female = “女” } let gender: Gender; gender = Gender.male; console.log(gender);
//编译后 var Gender; (function (Gender) { Gender[“male”] = “\u7537”; Gender[“female”] = “\u5973”; })(Gender || (Gender = {})); let gender; gender = Gender.male; console.log(gender);
<a name="2b87c202"></a>
### 枚举的规则
-
枚举的字段值可以是字符串或数字
-
数字枚举的值会自动自增
```tsx
enum Level{
level1 = 1,
level2,
level3
}//其中的level2的值为2,level3的值为3
被数字枚举约束的变量,可以直接赋值为数字(不推荐)
数字枚举的编译结果和字符串枚举有差异
最佳实践:
- 尽量不要再一个枚举中既出现字符串字段,又出现数字字段
- 使用枚举时,尽量使用枚举字段的名称,不要使用真实的值
修改扑克牌代码
// 创建一副扑克牌并打印
type Deck = NormalCard[];
enum Color{
heart = "♥",
spade = "♠",
club = "♣",
diamond = "♦"
}
enum Mark{
A = "A",
two = "2",
three = "3",
four = "4",
five = "5",
six = "6",
seven = "7",
eight = "8",
nine = "9",
ten = "10",
eleven = "J",
twelve = "Q",
King = "K"
}
type NormalCard = {
color:Color,
mark:Mark
}//对一张扑克牌的约束
function createDeck():Deck{
const deck: Deck = [];
const marks = Object.values(Mark);
const colors = Object.values(Color);
for(const m of marks){
for(const c of colors){
deck.push({
color: c,
mark: m
})
}
}
return deck;
}
function printDeck(deck: Deck){
let result = "\n";
deck.forEach((card, i) => {
let str = card.color + card.mark;
result += str + "\t";
if((i + 1) % 6 === 0){
result += "\n";
}
})
console.log(result);
}
const deck = createDeck();
printDeck(deck);
扩展知识:枚举的位运算
主要针对数字枚举
位运算:两个数字转换成二进制后的运算
enum Permission{
Read = 1, //0100
Write = 2, //0010
Create = 4, //0100
Delete = 8 //1000
}
// 1. 如何组合权限,使用或运算
// 0010
// 0001
// 或运算结果为0011
let p = Permission.Read | Permission.Write;
// 2. 如何判断是否拥有某个权限,可用且运算
// 0011
// 0010
// 且运算结果为0010
function hasPermission(target:Permission, per:Permission){
return (target & per) === per;
}
//判断变量p是否拥有可读权限
hasPermission(p, Permission.Read);
// 3. 如何删除某个权限
// 0011
// 0010
// 异或运算结果 0001
p = p ^ Permission.Write;
模块化
TS中,导入和导出模块,统一使用ES6的模块化标准,推荐使用正常导入和导出
在导入模块时,不可加入后缀名
编译结果
可配置tsconfig.json
TS中的模块化在编译结果中:
- 如果编译结果的模块化标准是ES6,则没有区别
- 如果编译结果的模块化标准是commonjs,则导出的声明会变成exports的属性,默认的到处会变成exports的default属性
解决默认导入的错误
import fs from "fs";
fs.readFileSync("./");
//以上代码会报错
解决方案
import * as fs from "fs"
//或
import {readFileSync} from "fs"
启用esModuleInterop
配置名称 | 含义 |
---|---|
module | 设置编译结果中使用的模块化标准 |
moduleResolution | 设置解析模块的模式 |
noImplicitUseStrict | 编译结果中不好含”use strict” |
removeComments | 编译结果移除注释 |
noEmitOnError | 错误时不生成编译结果 |
esModuleInterop | 启用es模块化交互非es模块导出 |
模块解析
模块解析:应该从什么位置寻找模块
TS中,有两种模块解析策略
- classic:经典
node:node解析策略(唯一的变化,是将js替换为ts)
- 相对路径
require("./xxx")
- 非相对模块
require("xxx")
- 相对路径
接口和类型兼容性
扩展类型-接口
接口:interface
扩展类型:类型别名、枚举、接口、类
TypeScript的接口:用于约束类、对象、函数的契约(标准)
契约(标准)的形式:
- API文档,弱标准
- 代码约束,强标准
接口与类型别名一样,不会出现在编译结果中
- 接口约束对象
interface User{ name: string age: number } let u: User = { name: "wjn", age: 33 }
- 接口约束函数 ```tsx interface User{ name: string age: number sayHello: () => void //sayHello():void }
//单独约束一个函数 // 1. 使用类型别名约束 type 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); // 2. 使用接口 interface Condition{ (n: number): boolean }
**接口可以继承**
`class Banner extends React.Component`
```tsx
interface A{
T1: string
}
interface B extends A{
T2: number
}
let u: B = {
T1: 11,
T2: 22,
T3: 33
}
可以通过接口之间的继承,实现多种接口的组合
使用类型别名可以实现类似的组合效果,需要通过&
,它叫做交叉类型
type A = {
name: string
}
type B = {
age: number
} & A
交叉类型与接口继承的区别
- 子接口不能覆盖父接口的成员
- 交叉类型会把相同的类型进行交叉
readonly
只读修饰符,表示修饰的目标是只读的
interface User{
readonly id: string//变量赋值后不可进行更改
name: string
age: number
}
const arr: readonly number[] = [3, 4, 6];
//上述arr赋值之后,不可更改元素内容,但可进行重新赋值
//只读数组还可通过ReadOnlyArray<number>来约束
TS中的类
面向对象思想
基础部分,学习类的时候,进讨论新增的语法部分
新增的类语法
属性
使用属性列表来描述类中的属性
属性的初始化检查
可在配置中添加"strictPropertyInitialization":true
属性的初始化位置:
- 构造函数中
- 属性默认赋值
属性可以初始化为可选的
属性可以修饰为只读的
使用访问修饰符:访问修饰符可以控制类中的某个成员的访问权限
- public:默认的访问修饰符,公开的,所有的代码都可以访问
- private:私有的,只有在类中可以访问
- protected
属性简写:如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写constructor(public name: string, age: number)
class User{
readonly id: number //属性可以修饰为只读的
public age: number
gender: "男"|"女" = "男" //属性的初始化位置
pid?: string//或pid: string | undefined //属性可以初始化为可选的
private publishNumber: number = 3; //举例:每天一共可以发布多少篇文章
private curNumber: number = 0; //举例:当天可以发布的文章数量
constructor(public name: string, age: number){
this.name = name;
this.age = age;
}
publish(title: string){
if(this.curNumber < this.publishNumber){
console.log("当前可以发送文章");
}else{
console.log("已达发送上限")
}
}
}
const u = new User("wjn", 25)
访问器
作用:控制属性的读取和赋值