阅读 3818

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

image.png

前言

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

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

基础类型

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

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

any null、undefined void never

工具

因为做实验的时候每次都需要tsc编译一下,然后node 文件 太麻烦了,我这里简单写了个gulp(没用webpack因为gulp比较简单方便并且快),大家愿意用可以直接用

评论区有人指出来只编译的话可以直接tsc -w 其实是一样的,不过我主要是为了自己方便watch的时候清屏,以及一切其他的文件操作,所以gulp方便一点,这里就留个架子,有兴趣可以在评论区或者gulp官网找更多方便的命令

用法:

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

package.json

{
  "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.js

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
复制代码
关注下面的标签,发现更多相似文章
评论