史上最全typescript使用指南

一、ts开发环境搭建

在node环境中搭建typescript,使用npm指令安装

复制代码
npm install -g typescript

然后使用tsc就可以使用编译ts文件

1.1 ts默认假设(设置)

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

  • 假设当前的执行环境是浏览器环境
  • 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行

举个例子

新建一个index.ts文件

bash 复制代码
let test:string = "123"

执行tsc index.ts,会生成一个index.js文件

然后index.ts文件会报错,test变量不能被重新声明,原因是因为,index.js文件中也有test变量,由于ts默认是全局执行,所以在index.ts中的test变量就不能被重新声明了

  • 编辑的目标代码是es3(最大的兼容性)

但是,我们可以手动的改变这些假设

1.2 改变ts默认设置(假设)

  • 使用ts命令行时加上参数(不推荐)
  • 使用ts配置文件,更改编译选项

二、ts配置文件

使用配置文件后,使用tsc编译时不能更上文件名了,跟上文件名就会忽略配置文件

新建ts配置有两种方式

  • 在根目录下手动新建tsconfig.json文件
  • 使用命令行的方式tsc --init生成tsconfig.json文件

tsconfig.json配置文件有许多的配置

2.1 compilerOptions配置编译选项

perl 复制代码
{
    "compilerOptions":{
            "target":"es2016",                  //js代码标准,默认为es3
            "module":"commonjs",                 //编译目标(编译后的js代码)使用的模块化标准,一般为commonjs(node环境),es6(浏览器环境)
            "lib":["es2016"],                    //ts运行的全局环境,比如es2016代表ts可以使用es2016这个库的环境,但是没有浏览器环境了,像console、document等这些都不再支持了,要解决这个问题就需要安装一个库@types/node。@types是ts官方的类型库,其中包含了很多对js代码的描述,像很多三方库没有ts类型描述,但是又需要类型检测,所有可以去@types中检测是否有。
            "outDir":"./dist"                    //编译后的输出文件目录
    },
    "include":["./src"],                                 //需要编译的文件
    "files":["./src/xx"]                                 //指定编译文件及其依赖文件
}

三、使用三方库简化流程

通过上面的案例知道,ts编译运行需要先执行编译命令,然后再使用node运行文件,并且同时文件变更后有需要重新编译运行,这个相当麻烦,因此有一些三方库来帮助我们简化这些流程。

3.1 ts-node

将ts代码在内存中完成编译,并且完成运行

arduino 复制代码
npm install -g ts-node             //全局安装

运行命令

arduino 复制代码
    ts-node 入口文件                 //注意这种方式不会输出编译后文件

3.2 nodemon

用于检测文件变化

全局安装

复制代码
npm install -g nodemon 
scss 复制代码
nodemon --exec ts-node src/index.ts     //该命令的作用是,当文件发生变化就执行后面的命令

添加配置,只要监控ts文件

-e 后面跟需要监控的文件类型

bash 复制代码
nodemon -e ts --exec ts-node src/index.ts     

--watch 后面跟监控的文件夹

css 复制代码
nodemon --watch src -e ts --exec ts-node src/index.ts

注意tsc有一个命令

tsc --watch

这个命令只会监听编译,但是不会执行编译文件,只要有文件变更就会出编译结果。

四、ts中使用模块化

4.1 新增的tsconfig.json配置

配置名称 含义
module 设置编译结果中使用的模块化标准
moduleResolution 设置解析模块的模式
noImplicitUseStrict 编译结果中不包含"use strict"
removeComments 编译结果移除注释
noEmitOnError 错误时不生成编译结果
esModuleInterop 启用es模块化交互非es模块化导出
experimentalDecorators 开启装饰器语法
noImplicitAny 开启隐式any检查
declaration 编译结果是否生成声明文件

4.2 回顾前端模块化标准

参考文章juejin.cn/post/749299...

4.2.1 ts中如何书写模块化语句?

在es6出来之后,ts就已经完全支持es标准,因此ts中导入导出统一使用es6的模块化标准即可。

4.2.2 编译结果是怎样的?

可在tsconfig.json文件里配置

json 复制代码
{
    "compilerOptions":{
            "module":"commonjs"             //配置编译目标使用的版本,commonjs、es6...
    },
}

注意

  • 如果编译结果的模块化标准是es6,则编译结果没有区别
  • 如果编译结果是commonjs,导出的声明会变成exports的属性,默认导出会变成exports的default属性

4.3 模块解析

4.3.1 什么是模块解析?

应该从什么位置寻找和解析模块?

4.3.2 模块解析策略

  • classic:经典模块解析策略(es6之前就已经出现,已经过时)
  • node:node解析策略(后面写node文章补充上)

推荐设置配置moduleResolution:"node"使用node解析

4.4 默认导出模块报错

在ts文件中直接书用fs这种默认导出模块会报错,比如

java 复制代码
import fs from "fs" 
fs.readFileSync("./")  //报错

//编译后文件
const fs_1 = require("fs")
    fs_1.default.readFileSync("./")  编译错了,因为fs模块不是用ts写的,fs这个模块使用的module.exports = {}

解决办法

javascript 复制代码
import {readFileSync} from "fs" 
readFileSync("./")  //不再报错

//编译后文件
const fs_1 = require("fs")
    fs_1.readFileSync("./") 

或者

javascript 复制代码
import * as fs from "fs"
fs.readFileSync("./")  //不再报错

//编译后文件
const fs_1 = require("fs")
    fs_1.readFileSync("./") 

或者

javascript 复制代码
//1、添加配置 esModuleInterop:true
import fs from "fs"
fs.readFileSync("./")  //不再报错

//编译后文件
会生成辅助函数,可自行查看编译结果

小结,即当在开发过程中如果遇到三方库使用的是module.export = {} 方式导出的,则使用上述办法解决

4.5 如何在ts中书写commonjs模块化代码

4.5.1 使用commonjs规范书写

javascript 复制代码
//a.js文件

module.exports = {
    name:"test"
}

//b.js文件
const myModule= require("./a")    //myModule 类型是any

上面代码会出现问题,类型检查没了,因此ts提供一种语法解决这个问题

4.5.2 ts中的语法

javascript 复制代码
//a.js文件
export = {
    name:"test"
}

//第一种语法,推荐
//b.js文件导入
//使用es6模块化导入,需要开启esModuleInterop为true
import myModule from "./a"        //myModule 类型是{name:string}

//第二种语法
//b.js文件导入
import myModule= require("./a")    //myModule 类型是{name:string}

更多的模块化相关的配置可以参考上面

五、ts基本数据类型

5.1 基础数据类型

  • number

  • string

  • boolean

  • array

    约束数组类型必须约束数组每一项item的类型

    比如数字类型数组 number[],字符串数组 string[]等等

    也可以这样鞋 Array<number>,同等效果

  • object

    这个对象只能约束对象,不能约束对象内部具体的值

  • null 和undefined

    这两种类型是其他类型的子类型,它们可以赋值给其他类型

    比如

    ini 复制代码
    let str:string = undefined;
    str.toUpperCase();                  //会报错

    为了避免上面情况发生,可以在tsconfig.json中配置ts的严格模式

    json 复制代码
    {
        "compilerOptions":{
            "strictNullChecks":true       //更加严格的空类型检查
        }
    },

    加上strictNullChecks后undefined和null就不能赋值给其他类型了,只能赋值给自身。

5.2 其他常用类型

  • 联合类型

就是一个变量既可以是一种类型又可以是另一种类型,比如

typescript 复制代码
let str:string|undefined;

多种类型任选其一,可以配合类型保护进行判断

类型保护:当对某个变量进行类型判断之后,在判断语句块中便可以确定它的确切类型,typeof可以触发类型保护

  • void类型

约束函数的返回值,函数不返回任何值

  • never类型

通常约束函数的返回值,表示函数永远不可能结束

比如抛出错误

typescript 复制代码
function throwError(msg:string):never{
    throw new Error(msg)
}

function alwaysDoSomething():never{
    whild(true){
    }
}
  • 字面量类型 明确约束变量的值

比如

bash 复制代码
let age:"18"|"20"; //age 只能取值 "18"和"20"

let arr:[]        //这也是字面量约束,表示永远只能取空数组

let obj:{}       //这也是字面量约束,表示永远只能取空对象
  • 元组类型

表示固定长度的数组,并且数组每一个项的类型都确定

  • any类型

绕过类型检查,所以any可以赋值给任意类型

六、扩展类型

6.1 字面量类型的问题

问题一 :字面量在做约束时会产生很多的重复代码(可使用使用类型别名解决)

比如

javascript 复制代码
    let gender:"男"|"女"
    gender = "男"
    
    function searchUser(g:"男"|"女"){}
  • 类型别名

    对已知的一些类型定义名称

    typescript 复制代码
    type 类型名称 = 具体类型
    
    type Gender ="男"|"女"
    let gender:Gender="男"
     function searchUser(g:Gender){}
    //定义了一个User类型

问题二:逻辑名称和真实的值产生了混淆,会导致修改真实值的时候产生大量的修改。

问题三:字面量类型不会进入到编译结果

比如

javascript 复制代码
   let gender:"男"|"女"
    gender = "男"                      //想象一下现在取值的地方有很多很多地方
    
    function searchUser(g:"男"|"女"){}

当有一天需要把男变成帅哥,把女变成美女,那改动就大了去了。这就是所谓了把真实值和逻辑值混淆了。使用枚举可以解决。

6.2 枚举

枚举通常用来约束变量的取值范围。

6.2.1 枚举定义语法

ini 复制代码
enum 枚举名 {
    枚举字段1 = 值1,
    枚举字段2 = 值2,
    ...
}

上面的问题可以如下解决

ini 复制代码
   enum Gender {
       male = "男",
       female = "女"
   }
    gender = Gender.male                    //想象一下现在取值的地方有很多很多地方,也没关系了

枚举会参与编译,并且会出现在编译结果中即出现在js中,表现为对象。

6.2.2 枚举的规则

  • 枚举的值可以是数字和字符串

    数字枚举有一些特点,数字枚举的值会自动自增,比如

    typescript 复制代码
    enum Number {
        one,
        two,
        ...     //one 的值为 1会自增, ....
    }

    被数字枚举约束的值,可以直接赋值给变量,一般不要这么做

    ini 复制代码
    enum Number {
        one=1
        two,
        ...     //one 的值为 1会自增, ....
    }
    let one:Number = 1    //等于 let let one:Number = Number.one
    one=2

    数字枚举和字符串枚举的编译结果有差异

    以上面为例子,编译后会出现类似这样的代码

    css 复制代码
         {
             one:1,
             two:2,
             1:"one",
             2:"two"
         }

注意

  • 尽量不要在一个枚举中既出现数字又出现字符串
  • 使用枚举值,尽量使用枚举字段名称,不要直接使用真实值

6.3 接口与类型兼容

什么是接口?

用于约束类、对象、函数的标准

注意接口不会出现在编译结果中即不会出现在编译后的js文件中。

6.3.1 约束对象

语法

csharp 复制代码
interface 名称 {

}

这个看起来和类型别名没啥区别,确实在约束对象上它们俩没啥区别,最大的区别在约束类上。建议约束对象使用接口。

6.3.2 约束函数

typescript 复制代码
interface Test {
    (n:number):boolean
}
//等于

type Test = (n:number)=>boolean

//等于
type Test ={
     (n:number):boolean
}

6.3.3 接口继承

使用extends关键字实现

kotlin 复制代码
interface A {
    T1:string
}
interface B extends A {

}          //B中有T1

interface C extends B,A {}    //可以继承多个

使用type也可以实现类似效果使用&符号,交叉类型,但是更加推荐使用接口的继承。

type的交叉类型和interface的继承有比较大的区别

interface继承不能重新覆盖父interface的成员,而交叉类型是可以的,但是交叉类型会合并相同成员类型交叉

补充 readonly修饰符

只读修饰符

csharp 复制代码
interface User {
   readonly id:string
}

const user:User ={
    id:"123"
}
user.id="234" //编译不能通过,因为这个变量为只读形式

只读修饰符也不在编译结果中

6.3.4 类型兼容性

ts中判断类型是否兼容(变量能够被赋值),使用的子结构辩型法:目标类型需要某一些特征,赋值的类型只要满足该特征即可。

  • 基本类型判断:完全匹配
  • 字面量类型判断:完全匹配
  • 对象类型:子结构辩型法

比如

kotlin 复制代码
interface Parent {
    bloodType: "A",
    feature:"圆脸"
}

const child = {
    bloodType:"A",
    feature:"圆脸",
    age:18,
    swimming:true
}

const parent:Parent = child    //赋值成功,因为child有parent的特征

但是如果直接赋值ts会报错,因为使用字面量赋值的时候,ts会进行严格判断

kotlin 复制代码
interface Parent {
    bloodType: "A",
    feature:"圆脸"
}


const parent:Parent =  {
    bloodType:"A",
    feature:"圆脸",
    age:18,
    swimming:true
}      //赋值失败,因为ts会进行严格判断
  • 函数类型:

    参数处理:传递给目标函数的参数可以少不能多

    返回值处理:返回值类型一定要匹配,要求返回必须返回,如果不要求返回,则随意

6.4 类

在ts中的属性类需要使用属性列表、比如

kotlin 复制代码
class User {
    constructor(name:string,age:number){
        this.name = name;              //会报错,因为缺少属性列表描述
        this.age = age;                //会报错,因为缺少属性列表描述
    }
}

正确写法

typescript 复制代码
class User {
    name:string
    age:number
    constructor(name:string,age:number){
        this.name = name;              
        this.age = age;                
    }
}

有时可能会写出这种很奇怪的代码

typescript 复制代码
class User {
    name:string
    age:number
}

这个User类有name和age的属性列表描述,但是在User中有没有地方使用即这个name和age没有进行初始化,值为undefined。可以通过在tsconfig.json中新增strictPropertyInitialization:true配置,检查是否初始化

6.4.1 设置class属性列表默认值

typescript 复制代码
class User {
    name:string = "zhangsan"
    age:number = 18
    constructor(name:string,age:number){
    }
}

由上可看,可以直接在属性描述列表中赋初值。

6.4.2 属性赋值修饰符

readonly,表示当前属性为只读

typescript 复制代码
class User {
    readonly name:string = "zhangsan"
    age:number = 18
    constructor(name:string,age:number){
    }
}

?,表示当前属性为可选

typescript 复制代码
class User {
    name?:string
    age:number = 18
    constructor(name:string,age:number){
    }
}

6.4.3 访问修饰符

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

  • public:默认的访问修饰符,公开的,代表所有代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:受保护的成员,只能在自身和子类中访问

上面这些修饰符只会存在ts中,编译后的代码是没有的。

ts提供了一种语法糖,当构造函数的参数传入进来被直接赋值的时候,可以使用一种特殊的语法糖

typescript 复制代码
class User {
    name:string
    age:number
    constructor(name:string,age:number){
        this.name = name;
        this.age = age;
    }
}

等于

typescript 复制代码
class User {
    constructor(public name:string,public age:number){
    }
}
//注意一定要加访问修饰符

6.4.4 访问器

作用

控制访问属性的读取和赋值

这里与es2016的语法一样 设置set和get

参考文章 juejin.cn/post/755391...

6.4.5 class作为实例类型的约束

在 TypeScript 中,类(class)可以直接作为类型约束,用于约束变量、泛型参数或函数参数的类型:

  • 类作为实例类型的约束

    类本身代表其实例的类型,因此可以直接用类名约束变量必须是该类的实例。

typescript 复制代码
    class Person {
      name: string;
      constructor(name: string) {
      this.name = name;
      }
   }

    // 约束变量必须是 Person 实例
    function greet(p: Person) {
      console.log(`Hello, ${p.name}`);
    }

    const person = new Person("Alice");
    greet(person); // ✅ 合法
    greet({ name: "Bob" }); // ❌ 报错:缺少 Person 类的构造逻辑
  • 类作为泛型约束

    在泛型中,可以通过 T extends Class 的形式约束泛型参数必须是类的实例类型。

scala 复制代码
class Animal {
  makeSound() {
    console.log("Some sound");
  }
}

function createInstance<T extends Animal>(ctor: new () => T): T {
  return new ctor();
}

class Dog extends Animal {
  makeSound() {
    console.log("Woof!");
  }
}

const dog = createInstance(Dog); // ✅ 返回 Dog 实例
dog.makeSound(); // 输出 "Woof!"
  • 类的构造函数类型约束

    如果需要约束泛型参数是类的构造函数 (而非实例),可以结合 new () => T 使用:

php 复制代码
function factory<T>(ctor: new () => T): T {
  return new ctor();
}

class User {
  id = Math.random();
}

const user = factory(User); // ✅ 返回 User 实例

6.5 泛型

谈起泛型先看一个典型的函数场景

ini 复制代码
function handleArray (arr:any[],n:number):any[] {
    if(n >= arr.length) return arr;
    const new Arr:any[] = [];
    for(let i = 0; i<n; i++){
        newArr.push(arr[i]);
    }
    return newArr
}
const newArr = handleArray([2,3,4,5],2)

上面这个操作数组的案例中,由于在声明handleArray处理方法时不知道用户会出进来怎样的数组,因此只能宽泛约束数组,如果用户传入的是一个字符串数组,也应该进行处理。总而言之就是对于数组的约束力度仍然是不够了。为了更加精确的约束,就需要范型了

泛型定义:是指附属于函数、类、接口、类型别名之上的类型,泛型相当于一个类型变量,只有当调用时才能确定类型

6.5.1 函数中使用泛型

在函数名之后写上 <泛型名称>

上述例子优化

ini 复制代码
function handleArray<T> (arr:T[],n:number):T[] {
    if(n >= arr.length) return arr;
    const new Arr:T[] = [];
    for(let i = 0; i<n; i++){
        newArr.push(arr[i]);
    }
    return newArr
}
const newArr = handleArray<number>([2,3,4,5],2)

按照上面的语法处理,handleArray在调用的时候就确定了要处理的数组类型了

同时在实际开发中当你使用泛型之后,ts也会很智能的推导数据类型

比如

scss 复制代码
const newArr = handleArray([2,3,4,5],2)
//不用传入number类型,ts会自动推导出数据类型

6.5.2 泛型默认值

可以在写泛型的地方这样写赋默认值

ini 复制代码
function handleArray<T = number> (arr:T[],n:number):T[] {
    if(n >= arr.length) return arr;
    const new Arr:T[] = [];
    for(let i = 0; i<n; i++){
        newArr.push(arr[i]);
    }
    return newArr
}

6.5.3 泛型在type、interface、class中的使用

直接在type、interface、class后写上泛型

r 复制代码
type test1<T> = (n:T,i:T) => boolean

interface test2<T> = {
    name:T
}

class Test3<T> {
    name:T
}

6.5.4 泛型约束

使用extends约束、比如下面案例

php 复制代码
interface test1 {
    name:string
}

function test2<T = extends test1> (value:T):T{
}
//value中一定有name属性

6.5.5 多范型

很简单就是传入多个范型值

php 复制代码
interface test1 {
    name:string
}

function test2<T = extends test1,K> (value:T,value2:K):T|K{
}

6.6 class的继承

class的继承主要是描述类之间的描述关系,同时解决开发中的重复代码。具体语法和es6一致

参考文章 juejin.cn/post/755391...

子类可以重写父类

  • 子类成员不能改变父类成员的类型

    scala 复制代码
    class Test1 {
        name = "test"
    }
    
    class Test2 extends Test1 {
        name = 123        // 错误,必须是字符串类型
        
    }
  • 无论属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配

    scala 复制代码
    class Test1 {
        name = "test"
        testfunction(){
            console.log("test1function")
        }
    }
    
    class Test2 extends Test1 {
        testfunction(){
            console.log("test2function")
        }
    }
    
    const obj = new Test2()
     obj.testfunction()  //test2function
  • 注意this的关键字,在继承关系中,this的指向是动态的,调用方法时,根据具体的调用者确定this指向

javascript 复制代码
    class Test1 {
        name = "test"
        testfunction(){
            console.log(`${this.name}`)
        }
    }
    
    class Test2 extends Test1 {
        name = 123
        testfunction(){
            console.log(`${this.name}`)
        }
    }
    
    const obj = new Test2()
     obj.testfunction()          //123

类型匹配

子结构变形法

  • 子类对象始终可以赋值给父类

单根性和传递性

单根性:每个类最多只能拥有一个父类 传递性:如果A是B的父类,并且B是C的父类,则可以认为A也是C的父类

6.7 抽象类

为什么需要抽象类? 以大家周知的中国象棋为例,可以根据中国象棋的棋子创建如下对象

scala 复制代码
    class Chess {}    //基础棋子对象
    class Horse extends Chess {}  //马
    class Pao extends Chess {}    //炮
    
    const horse = new Horse()
    const Pao = new Pao()

当代码写多了之后可能会出现以下情况

scala 复制代码
    class Chess {}    //基础棋子对象
    class Horse extends Chess {}  //马
    class Pao extends Chess {}    //炮
    
    const horse = new Horse()
    const Pao = new Pao()
    ....
    const chess = new Chess()  //语法没错,但是从面向对象的理解来说是不对的,因为中国象棋中并没有棋子这样的棋子,只有车、马、炮、兵等

为解决上述问题,抽象类诞生。语法,在Chess前面加上 abstract

scala 复制代码
    abstract class Chess {}    //基础棋子对象,加上abstract之后表示一个抽象概率,只作为提供子类公共代码,并没有实际意义
    class Horse extends Chess {}  //马
    class Pao extends Chess {}    //炮
    
    const horse = new Horse()
    const Pao = new Pao()
    ....
    const chess = new Chess()  //语法报错,抽象类无法创建实类

有时,某个类只表示一个抽象概率,主要用于提取子类共有成员,而不能直接传健它的对象。该类可以作为抽象类。给类前面加上abstract,表示该类是一个抽象类,不可创建抽象类对象。

6.7.1 抽象类成员

抽象类中可以有抽象成员,抽象成员只能出现在抽象类中

  • 父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或者实现是什么,因此需要一种强约束,让继承该类的子类必须要实现该成员。

    scala 复制代码
    abstract class Chess {
        abstract name:string           //抽象属性
        abstract getName:()=> void.    //抽象方法
    }
    class Horse extends Chess {
        name = "Horse"              //不重写会报错,因为name为抽象成员,必须复写实现
        getName(){
            console.log("getName")
        }
    }

6.8 再学接口(interface)

在上面对接口进行了一次定义,接口用于约束类、对象、函数是一个类型契约。当时并没有去约束类,同时接口与类型别名最大的区别就是接口可以和类进行联用

以一个案例来体会接口与类的联用

有一个马戏团,马戏团中有很多动物;包括:狮子、老虎、猴子、狗,这些动物都有共同的特别,拥有名字,年龄、种类名称。还有一个共同的方法:打招呼,它们各自有各自的技能,技能可以通过训练改变。

马戏团中有以下常见技能

  • 火圈表演:单火圈、双火圈
  • 平衡表演:独木桥、走钢丝
  • 智慧表演:跳舞、算术题

狮子和老虎能进行火圈表演

猴子能进行平衡表演

狗能进行智慧表演

以上背景,先使用面向对象的方式实现一次。不考虑接口的模式

scala 复制代码
abstract class Animal {
    abstract type:string;
    constructor(
        public name:string,
        public age:number
    ){
    }
    sayHi(){
        console.log("hi")
    }
}

class Lion extends Animal {
    type = "lion"
    singleFire () {
        console.log("singleFire")
    }
     doubleFire () {
        console.log("doubleFire")
    }
}

class Tiger extends Animal {
    type = "tiger"
    
    singleFire () {
        console.log("singleFire")
    }
     doubleFire () {
        console.log("doubleFire")
    }
    
}

class Monkey extends Animal {
    type = "monkey"
    
    dumoqiao () {
        console.log("dumuqiao")
    }
    zougansi () {
        console.log("zougangsi")
    }
}

class Dog extends Animal {
    type = "dog",
    suanshuti () {
        console.log("suanshuti")
    }
}

上面代码乍一看没问题,但是老虎和狮子有相同的能力,但是实现能力的方式不一样,上面代码没有体现出有相同的能力,比如老虎的跳火圈方法写成singleFire,狮子的跳火圈方法写成Fire,上面的代码也不会报错。

场景案例继续走

动物们登场了

arduino 复制代码
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]

动物们打招呼

javascript 复制代码
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    val.sayHello()
})

打招呼没问题,因此打招呼的方法继承父类,

但是让多有会进行火圈的动物,完成跳火圈表演,这就不好实现了,因为没有一个标识,表明哪些动物会跳火圈,只能这样写了

scala 复制代码
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    if(val instanceof Lion){
        val.singleFire()
        val.doubleFire()
    }else if (val instanceof Tiger){
        val.singleFire()
        val.doubleFire()
    }
})

上述代码有严重的隐患,因为判断条件是判断是不是会表演跳火圈,而实际的判断是判断老虎和狮子。当有一天马戏团来了新的狮子,此时狮子不会跳火圈,那这个判断条件问题出现了。

上述问题的根本原因在于

  • 对表演的能力没有强约束力

  • 容易将类型和能力耦合

系统中缺少对能力的定义 -- 接口

面向对象领域的接口的语意:表达了某个类是否拥有某种能力

使用interface对上面案例进行优化处理

scss 复制代码
interface IFireShow {
    singleFire():void;
    doubleFire():void;
}

interface IWisdomShow {
    suanshuti():void;
    tiaowu():void
}

interface IBalanceShow {
    dumuqiao():viod;
    zougangsi():viod
}

某个类具有某种能力其实就是实现接口,狮子具备跳火圈的能力

scala 复制代码
class Lion extends Animal implements IFireShow {
    type = "lion"
    singleFire () {
        console.log("singleFire")
    }                                  //不写,写错都会报错
     doubleFire () {
        console.log("doubleFire")
    }                                 //不写,写错都会报错
}

上述问题就解决了对表演的能力没有强约束力。

那么如何判断狮子是否具备有跳火圈的能力呢?使用类型保护处理

期望可以这样判断

scala 复制代码
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    if(val instanceof IfireShow){
        val.singleFire()
        val.doubleFire()
    }
})

但是这种判断是运行时,所以受限ts在运行时是不参与编译的,所以没法通过上面的方式处理,希望ts以后会优化吧

写个类型保护函数吧

typescript 复制代码
function hasFireShow(ani:object):ani is IFireShow {
    retun ((ani as unknown as IFireShow).singleFire && (ani as unknown as IFireShow).doubleFire)
}

再次优化代码

scala 复制代码
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    if(hasFireShow(val)){
        val.singleFire()
        val.doubleFire()
    }
})

还是有些戳,没办法,ts现在没有更好的办法,希望以后会优化吧

最后补充

  • 接口可以继承类,表示该类的所有成员都在接口中

    kotlin 复制代码
        class Test1 {
            a = " "
            b = " "
        }
        class Test2 {
            a1 = " ",
            b1 = " ",
        }
        interface Test3 extends A,B {
        }
        // 相当于
        interface Test3 {
         a:string
         b:string
         a1:string
         b1:string
        }
        

七、装饰器

装饰器是面向对象的概念,在java中叫注解,在c#中叫特征,在ts中称为装饰器decorator,在angular中大量使用,目前js是支持装饰器的,但是处于建议阶段。

7.1 解决的问题

  • 分离关注点 以一个背景为例,创建一个用户类,
c 复制代码
class User {
    id:string
}

现在有个需要需要验证用户的id不能大于10位数,第一种做法,创建一个验证函数

typescript 复制代码
class User {
    id:string
    ...
}
const user = new User()

const validateUser(u:User){
    if(user.id?.length<=10){
    ...
    }
}

上面代码看起来没什么问题,但是换种场景就会发现有一些问题了,比如现在有两位同事进行开发,a同事开发了User这个类,b同事使用这个类,那么a同事一定是更加清楚这个id的使用验证规则的。所以上面代码还是不够清晰,好的有些同学可能要又会想到,可以在class 中书写

typescript 复制代码
class User {
    id:string,
    validateUser(){
        if(this.id?.length<=10){
        ...
        }
    }
    ...
}
const user = new User()
user.validateUser()

上面代码看起来也没什么问题,但是细想,id这个属性还是和校验方法分开的当属性多了之后也是很烦的。

那么能不能在最开始定义这个id的时候就把校验规则加上呢?这种思维模式就是关注点的问题:在定义某些东西时,应该是最清楚该东西的情况的?

  • 重复代码问题

还是以上述为例,检验规则可能是5位也可能是15位等等,就会出现很多的重复if代码

上面两个问题产生的根源是,某些信息在定义时,能够附加的信息有限。 使用装饰器就可以为一些属性添加额外的信息

less 复制代码
class User {
    @required
    @range(5,15)
    id:string,
    validateUser(){
        if(this.id?.length<=10){
        ...
        }
    }
    ...
}

通过上面的代码就知道了,装饰器的作用,为某些属性,类,参数,方法提供元数据信息。

7.1.1装饰器的本质

在js中装饰器就是一个函数。装饰器是会参与运行的。

装饰器可以修饰:

  • 类成员(属性+方法)
  • 参数

7.2、类装饰器

类修饰器的本质也是一个函数,该函数接收一个参数,表示类本身(构造函数本身) 注意在ts中要使用装饰器,需要在tsconfig.json中开启experimentalDecorators

装饰器函数运行时间,在类定义后直接运行 语法

typescript 复制代码
@test1
class Test {
  constructor() {}
}

function test1(target: new () => object) {
  console.log(target);
}

上述代码编译后

javascript 复制代码
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
let Test = class Test {
    constructor() { }
};
Test = __decorate([
    test1
], Test);
function test1(target) {
    console.log(target);
}

约束函数参数为类的ts

  • new ()=>object 约束class没有参数的类
  • new (...args:any[])=>object 约束class有参数的类

类装饰器的返回值

  • void:仅运行函数
  • 返回新的类,会将新的类替换掉装饰器的目标类

多装饰器调用顺序是从下到上

7.3成员装饰器

成员装饰器分为属性和方法

7.3.1 属性装饰器

属性装饰器本质也是一个函数,该函数需要两个参数,

参数一

  • 如果是静态属性参数为类本身
  • 如果是实例属性,则为类的原型

参数二

  • 属性名

7.3.2 方法装饰器

方法装饰器本质也是一个函数,该函数需要三个参数,

参数一

  • 如果是静态方法,则为类本身
  • 如果是实例方法,则为类原型

参数二

  • 固定为一个字符串,表示方法名

参数三

  • 属性描述符对象

7.4 reflect-metadata库

作用,在装饰编写代码的过程中,如果需要存入一些数据,一些开发者可能会存在对象的原型上,这样就会污染原型,因此引入reflect-metadata用来外部存储这些数据

具体使用参考官方文档,比较简单

7.5 class-validator 和 class-transformer库

class-validator:对类做一些验证,不成功则返回错误数据

class-transformer:把平面对象转换成class对象

7.6 装饰器补充

7.6.1 参数装饰器

使用场景,依赖注入设计模式中使用

要求函数有三个参数:

  • 参数一

    • 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
  • 参数二

    • 方法名称
  • 参数三

    • 参数列表索引
less 复制代码
class Test {
    sum (a:number,@numberTest b:number) {
    
    }
}

function numberTest (target:any,method:string,index:number) {
}

依赖注入设计模式,后续一篇文章讲解

7.6.2 自动注入元数据

元数据:描述数据的数据

如果安装了reflect-metadata,并且导入了该库,并且在某个成员上添加了元数据。并且在tsconfig.json中启用了emitDecoratorMetadata 则ts在编译结果中,会将约束的类型,作为元数据加入到相应位置

这样一来ts的类型约束可以支持运行时了

目前使用场景很少、未来可能会发生大变化

7.6.3 AOP

属于面向对象的一种编程方式,将一些在业务中出现的功能块横向切分,分离关注点

八、索引器

css 复制代码
const obj = {
    a:123
}
console.log(obj["a"])

这种读取对象值使用[]方式读取值的方式就是类型表达式

注意

ts中不会对类型表达式做严格的类型检查

arduino 复制代码
class Test {
}
const test = new Test()

test["a"]      //不会报错,因此ts不会对类型表达式做严格的类型检查

如果期望做严格类型检查需要在tsconfig.jons中开启一个配置noImplicitAny:true

使用索引器代码修改

typescript 复制代码
class Test {
    [prop:string]:any   //它会限制当前类的所有属性
}
test["a"]              //也不会报错了

在索引器中键可以是数字(数组)和字符串

索引器的作用

  • 可以在严格的检查模式下,实现类动态增加成员
  • 可以实现动态操作类成员

九、类型计算

根据已知信息,计算新的类型

9.1 三个关键字

  • typeof

ts中的typeof书写位置在类型约束的位置上,获取某个数据类型比如

ini 复制代码
const a = "123"
const b:typeof a ="2" //会报错,因为 typeof a 会得到一个字面量类型 '123',所以b只能是 "123"

注意当typeof主用于类(class)时,得到的是其构造函数

  • keyof

作用于class,interface,type用于获取其他类型的中成员组成的联合类型,比如

typescript 复制代码
interface User {
    id:string,
    age:number
}
keyof User        // id:string | age:number
  • in

该关键字一般和keyof联用,限制某个索引的取值范围,比如

go 复制代码
type Obj = {
    [p in "id"|"age"]:string
}
//等于
type Obj = {
    id:string,
    age:string
}

const obj:Obj ={
    id:"123",
    age:"123",
    hah:"123" //会报错
} 

有了上述的三个关键字,就可以编写一些ts类型工具了

9.2 常见工具类型

Partial<T> 把范型T的所有属性都变成可选的

r 复制代码
type Partial<T>={
    [p in keyof T]?:T[p]
}

Required<T> 把范型T的所有属性都变成必选的

r 复制代码
type Required<T>={
    [p in keyof T]-?:T[p]
}

ReadOnly<T> 把范型T的所有属性都变成只读的

r 复制代码
type ReadOnly<T>={
   readonly [p in keyof T]:T[p]
}

String<T> 把范型T的所有属性都变成字符串

typescript 复制代码
type String<T>={
    [p in keyof T]:string
}

Pick<T, K>

css 复制代码
type Pick<T,K> = {
    [p in keyof T] : K[p]
}

T 中选取一组属性 K 构成新类型。

十、声明文件

10.1 什么是声明文件

.d.ts结尾的文件

10.2 声明文件的作用

为js代码提供类型声明

10.3 声明文件的位置

  • 放置到tsconfig.json包含的目录中
  • 放置到node_modules/@types文件目录中,一般都是npm安装的地方
  • 手动配置,在tsconfig.json文件中typeRoots中目录配置,这个配置之后,前面两种规则失效
  • 与js代码所在目录相同,并且文件名也相同的文件,这种格式其实就是ts工程发布之后的格式

10.4 编写声明文件

  • 手动编写

    手动编写的场景

    • 已有的库,它是使用的js书写而成,并且更改该库代码为ts成本较高
    • 对一些第三方的库,它们使用js写的,并且这些三方库没有提供声明文件,可以手动编写声明文件
  • 自动生成

    • 工程如果是使用ts生成的,发布编译后的js文件,如果发布的文件需要被别人使用,别人也需要类型检查,那么此时可以使用声明文件来描述发布结果结果中的类型
    • 此时ts工程就可以使用自动生成,在tsconfig.json中配置declaration即可

全局声明

typescript 复制代码
declare var console {
    log:(message?:any)=>void
}

namespace:表示命名空间,可以将其认为是一个对象,命名空间中的内容必须通过命名空间.成员名访问

typescript 复制代码
declare namespace console {
    log:(message?:any)=>void
}

十一、tslint

tslint和eslint差不多,tslint用来检查ts的代码规范的

一般安装到工程内

复制代码
npm install - D tslint typescript

初始化类型检查配置文件

csharp 复制代码
npx tslint --init

可以根据官网配置相应的检查规则

相关推荐
昨晚我输给了一辆AE8616 小时前
为什么现在不推荐使用 React.FC 了?
前端·react.js·typescript
Wect1 天前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
Dilettante2581 天前
这一招让 Node 后端服务启动速度提升 75%!
typescript·node.js
jonjia2 天前
模块、脚本与声明文件
typescript
jonjia2 天前
配置 TypeScript
typescript
jonjia2 天前
TypeScript 工具函数开发
typescript
jonjia2 天前
注解与断言
typescript
jonjia2 天前
IDE 超能力
typescript
jonjia2 天前
对象类型
typescript
jonjia2 天前
快速搭建 TypeScript 开发环境
typescript