史上最全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

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

相关推荐
zhangzelin8886 小时前
TypeScript入门指南:JavaScript的类型化超集
前端·javascript·其他·typescript
小高00712 小时前
🤔「`interface` 和 `type` 到底用哪个?」——几乎每个 TS 新手被这个选择灵魂拷问。
前端·javascript·typescript
TZOF13 小时前
TypeScript的新类型(五):tuple元组
前端·后端·typescript
TZOF13 小时前
TypeScript的object大小写的区别
前端·后端·typescript
TZOF13 小时前
TypeScript的对象如何进行类型声明
前端·后端·typescript
Kitasan Burakku2 天前
Typescript return type
前端·javascript·typescript
星光不问赶路人3 天前
一文搞清楚 TypeScript 中 triple-slash 与 tsconfig.types 有何区别?
typescript·vite
duansamve5 天前
React 18+TS中使用Cesium 1.95
react.js·typescript·cesium
岁岁岁平安6 天前
SpringBoot3+WebSocket+Vue3+TypeScript实现简易在线聊天室(附完整源码参考)
java·spring boot·websocket·网络协议·typescript·vue