为什么要学习TypeScript

  • 就业 或者 获得更大的竞争优势
  • 获得更好的开发体验
  • 解决js中一些难以处理的问题

JS开发中的问题

  • 使用了不存在的变量、函数或成员
  • 把一个不确定的类型当成一个确定的类型来进行处理
  • 在使用null或undefined的成员

js的原罪

  • js语言本身的特性,决定了该语言无法适应大型的复杂的项目
  • 弱类型:某个变量,可以随时更换类型
  • 解释性:错误发生的时间,是在运行时

前端开发中,大部分的时间都是在排错

TypeScript

简称TS

TypeScript是JS的超集,是一个可选的、静态的类型系统

  • 超集
    整数、正整数,整数是正整数的超集

  • 类型系统
    对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查

  • 可选的
    学习曲线非常平滑,即ts中新增的内容在开发过程中可用可不用

  • 静态的
    无论是浏览器环境,还是node环境,无法直接识别ts代码

    babel:es6 -> es5

tsc:ts -> js


tsc:ts编译器
静态:类型检查发生的时间,在编译的时候,而非运行时
TS不参与任何运行时的类型检查

TS的常识

  • 2012年微软发布
  • Anders Hejlsberg 负责开发TS项目
  • 开源、拥抱ES标准
  • 版本,目前为4.3
  • 中文官网 个人翻译

额外的惊喜

有了类型的检查,增强了面向对象给的开发

js中也有类和对象,js支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题。

使用TS后,可以编写出完善的面向对象代码

node环境搭建ts开发环境

安装TypeScript

建议全局安装

  1. npm install -g typescript

编译ts文件,在编译完成后,当前目录下会生成一个同名的js文件,即为ts代码的编译结果

tsc xxx.ts

默认情况下,TS会做出下面几种假设

  1. 假设当前的执行环境是dom
  2. 如果代码中没有使用模块化语句(import、export),便认为改代码是全几乎执行的
  3. 编译的目标代码是ES3

有两种方式更改以上假设:

  1. 使用tsc命令行的时候,加上选项参数
  2. 使用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”){
      return a * b;
      
      }else if(typeof a === “string” && typeof b === “string”){
      return a + b;
      
      } throw new Error(“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;
    }
}

扑克牌小练习

  1. 目标:创建一副扑克牌(不包含大小王),打印该扑克牌 ```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++){
     deck.push({
         mark: i,
         color: "♠"
     });
     deck.push({
         mark: i,
         color: "♥"
     });
     deck.push({
         mark: i,
         color: "♦"
     });
     deck.push({
         mark: i,
         color: "♣"
     });
    
    } return deck; }

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文档,弱标准
  • 代码约束,强标准

接口与类型别名一样,不会出现在编译结果中

  1. 接口约束对象
    interface User{
     name: string
     age: number
    }
    let u: User = {
     name: "wjn",
     age: 33
    }
    
  1. 接口约束函数 ```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

属性的初始化位置:

  1. 构造函数中
  2. 属性默认赋值

属性可以初始化为可选的

属性可以修饰为只读的

使用访问修饰符:访问修饰符可以控制类中的某个成员的访问权限

  • 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)

访问器

作用:控制属性的读取和赋值