浅谈TypeScript类型、接口、装饰器

image.png

前言

TypeScript 对于前端人员甚至后台人员都不算是特别陌生的东西了(身边不少java朋友看ts都以为还不错),今天来聊聊这玩意,基础用法,以及项目中你们都是怎么用的。 顺便一说,ts这东西实在是太大了,由于他的类型能够很灵活的去组合,不过放心,本文不会涉及太多概念性的东西(也说不完),由于其实一个大项目,每每就是这些基本类型用得最多,像type和interface这些东西,不过也分职位,若是你是项目组长或者是大家公司前端负责人要求的特别严格或者你是写工具的,封装一些公用组件的,用这些特别的东西机会会比较多javascript

在ts里,类型最好是规定的越死越好,由于这东西自己就是来规范本身规范团队的,若是要是全用any的话,不如直接用js,若是项目只有本身,那就更不必上这玩意了,给本身找麻烦前端

基础类型

ts里你们多少应该都听过,一些number类型string类型包括函数的参数类型什么nerver、void本文就再也不多赘述了,由于这东西实在太简单了。。。这里就简单的列一下vue

基本类型: number/string/boolean/Array/objectjava

any null、undefined void nevernode

工具

由于作实验的时候每次都须要tsc编译一下,而后node 文件 太麻烦了,我这里简单写了个gulp(没用webpack由于gulp比较简单方便而且快),你们愿意用能够直接用webpack

评论区有人指出来只编译的话能够直接tsc -w 实际上是同样的,不过我主要是为了本身方便watch的时候清屏,以及一切其余的文件操做,因此gulp方便一点,这里就留个架子,有兴趣能够在评论区或者gulp官网找更多方便的命令web

用法:typescript

  1. cnpm i -g gulp-cli  - 安装gulp自己
  2. cnpm i  - 安装本地依赖库
  3. gulp watch - 运行gulp的watch任务

package.jsonnpm

{
  "name": "test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "gulp": "^4.0.2",
    "gulp-clean": "^0.4.0",
    "gulp-run": "^1.7.1",
    "gulp-typescript": "^6.0.0-alpha.1",
    "gulp-watch": "^5.0.1",
    "typescript": "^3.7.4"
  }
}

复制代码

gulpfile.jsjson

const gulp = require('gulp'),
  watch = require('gulp-watch'),
  ts = require('gulp-typescript'),
  run = require('gulp-run'),
  clean = require('gulp-clean');

gulp.task('watch', () => {
  return watch('./1.ts', () => {
    gulp
      .src('./1.ts')
      .pipe(
        ts({
          target: 'ES6',
          outFile: '1.js',
          experimentalDecorators: true,
        }),
      )
      .on('error', err => {
        // console.error(err);
      })
      .pipe(gulp.dest('build'))
      .pipe(run('node build/1.js'));
  });
});

复制代码

项目结构以下

image.png

运行结果

这个build不用本身手动建立,直接运行gulp,会自动建立而且运行的

image.png

数组

数组的花样其实还相对来讲挺多的,这里先介绍基础用法,下文会讲到和其余的一些东西配合

let arr = Array<number>; // 规定只能是装数字的数组
// 能够简写成 let arr = number[];
复制代码

Type - Interface

常常有人问我 ts type 和 interface 有啥区别,首先确定一点,这俩功能确定是有相同点,也确定有区别,要否则做者也就不有病搞两个出来了,这里咱们先说分别的用法,再说区别

我们暂时理解成这两个都是自定义类型的

首先ts约定类型是能够约定json内部的,这个你们都知道就像这样 这样是报错的

image.png

严格遵照才能够

image.png

type

可是若是有一个类型特别经常使用,好比说是用户类型,假设也是确定有名字和年龄,每次定义变量的时候写一堆那确定是不行

image.png

这样就能够在多个地方用了

interface

上面的例子直接改为interface也是同样的

image.png

区别

其实这么一看例子,你们可能会想,这不同么,有啥区别

image.png
其实不是,这个interface严格翻译来讲叫 接口  其实这个东西我们前面那种用法根本是不对的,也不能说不对,能够说是否是真正适合他的地方,这东西不是当类型用的,真正的用法是 须要把他用于实现  可能这时候有人会以为,说了跟没说同样,怎么就实现,实现什么啊,说人话等等

想象如今有个需求,我有一个类,这个累的是对http请求的封装,这里面能够直接把数据变成字符串发到服务器而后还能够拿回来的时候再解析成json,在写这个例子前,咱们先来介绍另外一个东西

implements

implements  这个东西跟extends有点像,extends是从一个类继承出来,这个implements不是继承一个类,而是实现一个接口

那么结合interface我们来写个例子

interface serializeable {
  tostring(): string;
  fromString(str: string): void;
}

class SendData implements serializeable {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public tostring() {
    return JSON.stringify({
      name: this.name,
      age: this.age,
    });
  }

  public fromString(str: string) {
    let data = JSON.parse(str);
    this.name = data.name;
    this.age = data.age;
  }
}

复制代码

顺便一说这个,implements是能够同时实现多个接口的,就直接跟名字就能够了,像这样

interface serializeable {
  tostring(): string;
  fromString(str: string): void;
}

interface serializeable2 {

}

class SendData implements serializeable, serializeable2 {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public tostring() {
    return JSON.stringify({
      name: this.name,
      age: this.age,
    });
  }

  public fromString(str: string) {
    let data = JSON.parse(str);
    this.name = data.name;
    this.age = data.age;
  }
}

复制代码

看到这。。。我相信有人还有疑问,那。。。。这东西到底怎么用呢

参考我们上面提到的 http的那个需求,尝试用一下这玩意,假设,如今要发送给服务器的数据必须得实现我这个接口

interface serializeable {
  tostring(): string;
  fromString(str: string): void;
}

class SendData implements serializeable {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public tostring() {
    return JSON.stringify({
      name: this.name,
      age: this.age,
    });
  }

  public fromString(str: string) {
    let data = JSON.parse(str);
    this.name = data.name;
    this.age = data.age;
  }
}

function sendToServer(obj: serializeable) {}

sendToServer(new SendData('name', 18));

复制代码

这时候看控制台是没有任何错误的

可是我们随便换一个类

image.png

我这个SendData2没有实现个人接口,这个时候他是否是就直接爆了呀,可能仍是会有人以为,那。。。。这有什么用呢? 注意⚠️这个其实对于挑错颇有用,正常状况下,用js去写的话,是否是须要在 sendToServer 这个函数里执行 obj.tostring 或者这个接口的其余方法才会保存,那这个就变成运行时的报错了,若是项目大起来,找错是特别麻烦的事,因此这个东西能直接避免一部分错误,真不错对吧?

泛型

泛型稍微有点特殊,我们先来看个例子,更能让你们明白这东西的做用 这里先写一个函数,先不考虑实用性,就是有一个函数,能够传进去一个数字,和循环的次数,而后返回一个数字数组

function repeat(item: number, count: number): number[] {
  let result: number[] = [];

  for (let i = 0; i < count; i++) {
    result.push(item);
  }

  return result;
}

let arr: number[] = repeat(13, 4);
console.log(arr);

复制代码

image.png

首先东西确定是能出来,可是。。。。 repeat是吧,如今实现的只是循环数字,有没有可能未来须要循环字符串,布尔值,那这个一个一个写得写多少 因此我们如今得想办法把类型传过来,固然了 any固然也能够,不过。。。。参考我写的前言那里 若是用any的话干脆用js算了 固然了,我们这主要说明泛型,其实在这用any也是能够的

image.png

function repeat<T>(item: T, count: number): T[] {
  let result: T[] = [];

  for (let i = 0; i < count; i++) {
    result.push(item);
  }

  return result;
}

let arr: number[] = repeat<number>(13, 4);
console.log(arr);

let arr2: string[] = repeat<string>('aaa', 4);
console.log(arr2);
复制代码

image.png

是否是很简单呢?

其实,你们看这东西有没有感受眼熟,是的没错,在ts里数组就是一个泛型,好比 Array<string>  而且,再引入一个概念就是

类型推测

就这刚才我们的例子来讲 其实直接不传类型,也是能够出来的

image.png

ts是很聪明的,他能够本身根据你传过来的类型,来推测你是什么类型,固然了,该简的简,稍微复杂点的,好比说一个泛型类,内部声明个数组,而后有个add方法,第一次是数字,第二次是字符串那他就推测不出来了,过份了确定不行。 其实这个泛型提及来,很是的庞大,这个你们若是感兴趣能够留言或者评论单开一章专门讲它,由于这个泛型有一些变种,比方说有多两个泛型,三个的,分别用在什么地方,并且还能够有可选的类型,替换的类型,联合的类型,交叉的类型,烂七八糟稀奇古怪的东西多了去了,因此。。。有兴趣的话你们留言哈~

装饰器

这个装饰器其实我我的是很喜欢用的,他能够直接给class附加一些功能 其实这是有有人可能会有疑问,为啥我要用这玩意加,我直接加上不就完了么,还省事,其实能够想象一下,如今须要用的用户数据附加到我这个class身上,首先确定一点,挨个加确定是能够的,可是就是麻烦么,俗话说得好,懒是推动人类进步最大的动力么不是。 其实若是了解vue 2.x ts版的应该知道,他就是充满了装饰器的写法(顺便一说,目前vue3放出来的消息是抛弃了这个装饰器的写法了,多是由于这东西暂时是实验性特性,具体还须要等后面通知)

image.png

看了vue的用法,我们先来看看简单的装饰器该怎么写

类装饰器

其实装饰器就是一个函数,而后直接加一个@符放到class上就能够了,注意须要注意参数,要么ts会给你报错

注意⚠️这个fn只有在类装饰器的时候才会只有一个参数,在属性和方法的时候不同,这个下文会说

function fn(target){

}

@fn
class User {

}
复制代码

顺便一提,若是你用的是vscode或者是什么其余的编辑器的话,可能会给你报错

image.png
这个也就是说刚才提到的实验版功能的缘由 看着碍眼的话能够直接新建一个  tsconfig.json

{
  "compilerOptions": {
      "target": "ES5",
      "experimentalDecorators": true
  }
}
复制代码

写上这个,就不会报错了

其实这个 target 就是我们的类的构造函数

image.png
这个时候是否是就简单了,我们先直接给target加个属性,像这样

function fn(target) {
  console.log(target);
  target.a = 12;
}

@fn
class User {}

console.log(User.a);

复制代码

这时候看结果会发现报了一个错,而且结果还出来了,这个就很奇怪

image.png

事实上来讲ts是很严格的,他必须在初始化的时候就得用,运行时是没问题,结果也正常,可是人家就是检测不到 那。。。怎么办呢? 很简单,我们其实直接在类上定义这个属性就能够了,像这样

function fn(target) {
  console.log(target);
  target.a = 12;
}

@fn
class User {
  static a: number;
}

console.log(User.a);

复制代码

这时候再运行,就不会报错了

image.png

装饰器传参

其实这个装饰器传参还稍微有点特殊,这个target(也就是 constructor)会传到函数return出来的函数内部,而最外层才是传进来的参数,来看下代码

function fn(num:number) {
  return function(constructor: Function){
    constructor.prototype.a = num
  }
}

@fn(12)
class User {
  a: number;
}

let obj = new User();
console.log(obj.a);

复制代码

运行时会看到

image.png

成功了~ 很开心 而如今,我们直接写两个类,就能够经过传参数来区分了

function fn(num:number) {
  return function(constructor: Function){
    constructor.prototype.a = num
  }
}

@fn(12)
class User {
  a: number;
}

let obj = new User();
console.log(obj.a);

@fn(5)
class User2 {
  a: number;
}

let obj2 = new User2();
console.log(obj2.a);


复制代码

结果: 

image.png

真棒,对不对

进阶

其实装饰器到上一步已经能知足大部分人的工做需求了,由于这个东西

  1. 是个实验性的东西
  2. 工做中其实不多能用到

可是,仍是有点小东西挺有意思,顺便来分享一下

前言:我们这个类,确定是不知一个实例对吧,那么接下来,我们这么写,直接一个属性一点毛病没有,可是。。。万一是个json呢,我们来看个例子

image.png

改为json后,到这步还没什么错,接下来

image.png

GG了,是否是改其中一个属性另外一个也跟着一起改了呀,这也就是prototype这种方式的不完整,因此千万别用刚才的那种装饰器传餐来写真是项目,会出人命的。 可是怎么办呢。。。 给谁加都不对,直接说正确作法了,能够直接把以前的那个类,给它重写了,可是有一个问题,又不能直接复制代码再来一套,废话很少,直接上代码

function fn(num: number) {
  return function<T extends {new(...arg:any[]):{}}>(constructor: T) {
    return class extends constructor {
      json: object = { a: num };
    };
  };
}

@fn(12)
class User {
  json: {
    a: number;
  };
}

let obj = new User();
obj.json.a = 80;
console.log(obj.json);

let obj2 = new User();
console.log(obj2.json);

复制代码

image.png

这个时候,就没问题了~是否是很简单 固然。。。放下大家手中的刀,可能有人会说,等会等会,这玩意怎么就忽然变成这一大坨东西了,什么 <T extends {new(...arg:any[]):{}}> 这都什么玩意啊,对不对?

实际上是这样的,我们能够先抛弃这个来看一眼

image.png
会看到一个错误,说这个constructor不是一个一个function, 由于它多是一个 User,多是任何一个类,那怎么办呢? 因此,我们须要一个泛型函数

image.png

可是直接写T也不行,直接说了,我们这个T要继承一个接口,而且这个接口还不能是一个普通的接口,还须要是一个动态的接口,因此须要动态的建立一个函数  也就是上面代码的 new () 而且呢,这里面确定是什么参数都有,因此全拿出来 new(...args:any[]) 什么类型都有这种时候给一个 any 就能够,而且还须要返回一个{},结合起来就是 <T extends {new(...arg:any[]):{}}>  是否是很简单呢~~~~~

好的,本节ts教程就写到这了,你们有什么问题欢迎在评论区评论噢 或者能够加个人qq和微信,我们一块儿沟通 qq:

916829411
复制代码

微信:

Dyy916829411
复制代码
相关文章
相关标签/搜索