Vue(十二)-TypeScript在Vue中的使用

2,737 阅读15分钟

一、环境准备

Ts是ES6的超集,浏览器不认识ES6和Ts代码,安装ts

> npm install -g typescript

在命令行上,运行TypeScript编译器:

tsc greeter.ts

输出结果为一个greeter.js文件,它包含了和输入文件中相同的JavsScript代码。

如果使用vue-cli的话不用配置IDE,它会自动配置,如果自己配置的话,输入

tsc --init

生成tsconfig.json

二、ts的类型注解

ts中数据类型的用法,在js中我们只是知道一个东西是某个类型,但我们从来没有声明过,在ts中我们需要声明一个变量到底是什么类型,变量后加冒号+空格

1. 布尔值

2. 数字

3. 字符串

4.1 伪数组

伪数组不是数组,得用接口

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
    let args: IArguments = arguments;
}

IArguments 是 TypeScript 中定义好了的类型

4.2 数组

有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组,第二种方式是使用数组泛型,Array<元素类型>,

let isDone: boolean = false;

let decLiteral: number = 6;

let name: string = "bob";

let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3];

5. 元组 Tuple

元组类型允许表示一个已知元素数量和类型数组,各元素的类型不必相同。 

let x: [string, number];

6. 枚举

enum类型是对JavaScript标准数据类型的一个补充。

enum Color {Red, Green, Blue}
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

枚举就是列出一种东西所有的可能性

7. Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量。在any类型上调用没有的方法也不会报错

8.Void

void类型像是与any类型相反,通常用于函数,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

function warnUser(): void {
    console.log("This is my warning message");
}

9. Null 和 Undefined

let u: undefined = undefined;
let n: null = null;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。 再次说明,稍后我们会介绍联合类型。

--strictNullChecks标记可以在tsconfig.json中设置

10. Never

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。

11. Object

object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。 使用object类型,就可以更好的表示像Object.create这样的API。

12. 类型断言

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

13.类型推断

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

14.联合类型

联合类型使用 | 分隔每个类型。当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

一个变量只能被推断为一种值

三、函数

函数在Ts中的写法

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

输入多余的(或者少于要求的)参数,是不被允许的

完整函数类型

let myAdd: (baseValue: number, increment: number) => number =
    function(x: number, y: number): number { return x + y; };

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

TypeScript里的每个函数参数都是必须的。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。 比如,我们想让last name是可选的:

function buildName(firstName: string, lastName?: string) {
    // 选择传参
}
和

function buildName(firstName: string, lastName = "Smith") {
    // 带有默认值
}

三点运算符:可以接受无限个单个参数作为数组

function sum(...num: number[]) {
    let result = 0;
    for(i=0;i<=num.length;i++){
        result+=i;    
    }
    return result
}
sum(1,2,3,2,2,2)
rest 参数只能是最后一个参数

num是数组,但是sum的参数是一个个的数字

函数可以利用typeof进行重载

接口定义函数

interface SearchFunc {    (source: string, subString: string): boolean;}

四、接口-对象的类型

Interface 是一种描述对象或函数的东西。你可以把它理解为形状,一个对象需要有什么样的属性,函数需要什么参数或返回什么样的值,数组应该是什么样子的,一个类和继承类需要符合什么样的描述等等。

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

type 与 interface 类似

type 创建类型别名

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

字符串字面量类型,字符串字面量类型用来约束取值只能是某几个字符串中的一个,type EventNames = 'click' | 'scroll' | 'mousemove';

对象 Interface

  1. 设置需要存在的普通属性
  2. 设置可选属性
  3. 设置只读属性
  4. 另外还可以通过 as 或 [propName: string]: any 来制定可以接受的其他额外属性,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
interface Person {
    name: string
    bool?: boolean
    readonly timestamp: number
    readonly arr: ReadonlyArray<number> // 此外还有 ReadonlyMap/ReadonlySet
}

let p1: Person = {
    name: 'oliver',
    bool: true, // ✔️️ 可以设置可选属性 并非必要的 可写可不写
    timestamp: + new Date(), // ✔️ 设置只读属性
    arr: [1, 2, 3] // ✔️ 设置只读数组
}

let p: Person = {
    age: 'oliver', // ❌ 多出来的属性
    name: 123 // ❌ 类型错误
}

p1.timestamp = 123 // ❌ 只读属性不可修改
p1.arr.pop() // ❌ 只读属性不可修改

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}
任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。

函数 Interface

Interface 还可以用来规范函数的形状。Interface 里面需要列出参数列表返回值类型的函数定义。写法如下:

  1. 定义了一个函数接口
  2. 接口接收三个参数并且不返回任何值
  3. 使用函数表达式来定义这种形状的函数
interface Func {
    // ✔️ 定于这个函数接收两个必选参数都是 number 类型,以及一个可选的字符串参数 desc,这个函数不返回任何值
    (x: number, y: number, desc?: string): void
}

const sum: Func = function (x, y, desc = '') {
    // const sum: Func = function (x: number, y: number, desc: string): void {
    // ts类型系统默认推论可以不必书写上述类型定义
    console.log(desc, x + y)
}

sum(32, 22)

可索引类型 Interface

这种 Interface 描述了索引类型的形状,规定索引返回的值的类型

interface StringSet {
    readonly [index: number]: string // ❗ 需要注意的是 index 只能为 number 类型或 string 类型
    length: number // ✔️ 还可以指定属性
}

let arr1: StringSet = ['hello', 'world']
arr1[1] = '' // ✔️ 可以设置为只读防止给索引赋值
let arr: StringSet = [23,12,3,21] // ❌ 数组应为 string 类型
interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。

虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。

不过有一种情况例外,那就是它常用来表示类数组。

类 Interface

Interface 也可以用来定义一个类的形状。需要注意的是类 Interface 只会检查实例的属性,静态属性是需要额外定义一个 Interface;比如:

// 🥇 PersonConstructor 是用来检查静态部分的
interface PersonConstructor {
    new (name: string, age: number) // ✔️ 这个是用来检查 constructor 的
    typename: string // ✔️ 这个是用来检查静态属性 typename 的
    logname(): void // ✔️ 这个用来检查静态方法 logname 的
}
// 🥈 PersonInterface 则是用来检查实例部分的
interface PersonInterface {
    // new (name: string, age: number) // ❌ 静态方法的检查也不能写在这里 这样写是错误的
    log(): void // : 这里定义了实例方法 log
}

// class Person implements PersonInterface, PersonInterface { ❌ 这样写是错误的
const Person: PersonConstructor = class Person implements PersonInterface {
    name: string
    age: number
    static typename = 'Person type' // 这里定义了一个名为 typename 的静态属性
    static logname() { // 这里定义了一个名为 logname 的静态方法
        console.log(this.typename)
    }
    constructor(name: string, age: number) { // constructor 也是静态方法
        this.name = name
        this.age = age
    }
    log() { // log 是实例方法
        console.log(this.name, this.age)
    }
}

⚠️ 一定要记住静态属性和方法的检查、实例属性和方法的检查是不同的 Interface

Interface 的继承

跟 class 一样,使用 extens 继承,更新新的形状,比方说继承接口并生成新的接口,这个新的接口可以设定一个新的方法检查。

看个例子🌰:

interface PersonInfoInterface { // 1️⃣ 这里是第一个接口
    name: string
    age: number
    log?(): void
}

interface Student extends PersonInfoInterface { // 2️⃣ 这里继承了一个接口
    doHomework(): boolean // ✔️ 新增一个方法检查
}
interface Teacher extends PersonInfoInterface { // 3️⃣ 这里又继承了一个接口
    dispatchHomework(): void // ✔️ 新增了一个方法检查
}

// interface Emmm extends Student, Teacher // 也可以继承多个接口

let Alice: Teacher = {
    name: 'Alice',
    age: 34,
    dispatchHomework() { // ✔️ 必须满足继承的接口规范
        console.log('dispatched')
    }
}

let oliver: Student = {
    name: 'oliver',
    age: 12,
    log() {
        console.log(this.name, this.age)
    },
    doHomework() { // ✔️ 必须满足继承的接口规范
        return true
    }
}
复制代码

混合类型的 Interface

混合类型的接口就是使用同一个 Interface 来描述函数或者对象的属性或方法,比如一个函数接收什么参数,输出什么结果,同时这个函数有另外什么方法或属性之类的。🌰

interface Counter {
    (start: number): void // 1️⃣ 如果只有这一个那么这个接口是函数接口
    add(): void // 2️⃣ 这里还有一个方法,那么这个接口就是混合接口
    log(): number // 3️⃣ 这里还有另一个方法
}

function getCounter(): Counter { // ⚠️ 它返回的函数必须符合接口的三点
    let count = 0
    function counter (start: number) { count = start } // counter 方法函数
    counter.add = function() { count++ } // add 方法增加 count
    counter.log = function() { return count } // log 方法打印 count
    return counter
}

const c = getCounter()
c(10) // count 默认为 10
c.add()
console.log(c.log())
复制代码

继承类的 Interface

Interface 不仅能够继承 Interface 还能够继承类,再创建子类的过程中满足接口的描述就会必然满足接口继承的类的描述

class Person {
    type: string // ❗️这里是类的描述
}

interface Child extends Person { // ❗️Child 接口继承自 Person 类,因此规范了 type 属性
    log(): void
    // 这里其实有一个 type: string
}

// ⚠️ 上面的 Child 接口继承了 Person 对 type 的描述,还定义了 Child 接口本身 log 的描述

// 🥇 第一种写法
class Girl implements Child {
    type: 'child' // 接口继承自 Person 的
    log() {} // 接口本身规范的
}

// 🥈 第二种写法
class Boy extends Person implements Child { // 首先 extends 了 Person 类,然后还需满足 Child 接口的描述
    type: 'child'
    log() {}
}

这个接口的定义和使用如下图所示:

20190318234436.png

五、VUE

1、vue-property-decorator

这里单页面组件的书写采用的是 vue-property-decorator 库,该库完全依赖于 vue-class-component ,也是 vue 官方推荐的库。

单页面组件中,在 @Component({}) 里面写 propsdata 等调用起来极其不方便,而 vue-property-decorator 里面包含了 8 个装饰符则解决了此类问题,他们分别为

  • @Emit 指定事件 emit,可以使用此修饰符,也可以直接使用 this.$emit()
  • @Inject 指定依赖注入)
  • @Mixins mixin 注入
  • @Model 指定 model
  • @Prop 指定 Prop
  • @Provide 指定 Provide
  • @Watch 指定 Watch
  • @Component export from vue-class-component

举个🌰

import {
  Component, Prop, Watch, Vue
} from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
  dataA: string = 'test'
    
  @Prop({ default: 0 })
  propA: number

  // watcher
  @Watch('child')
  onChildChanged (val: string, oldVal: string) {}
  @Watch('person', { immediate: true, deep: true })
  onPersonChanged (val: Person, oldVal: Person) {}

  // 其他修饰符详情见上面的 github 地址,这里就不一一做说明了
}
复制代码

解析之后会变成

export default {
  data () {
    return {
      dataA: 'test'
    }
  },
  props: {
    propA: {
      type: Number,
      default: 0
    }
  },
  watch: {
    'child': {
      handler: 'onChildChanged',
      immediate: false,
      deep: false
    },
    'person': {
      handler: 'onPersonChanged',
      immediate: true,
      deep: true
    }
  },
  methods: {
    onChildChanged (val, oldVal) {},
    onPersonChanged (val, oldVal) {}
  }
}
复制代码

2、vuex-class

vuex-class 是一个基于 VueVuexvue-class-component 的库,和 vue-property-decorator 一样,它也提供了4 个修饰符以及 namespace,解决了 vuex 在 .vue 文件中使用上的不便的问题。

  • @State
  • @Getter
  • @Mutation
  • @Action
  • namespace

copy 一个官方的🌰

import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}
复制代码

到这里,ts 在 .vue 文件中的用法介绍的也差不多了。我也相信小伙伴看到这,对其大致的语法糖也有了一定的了解了

3、一些建议

  • 如果定义了 .d.ts 文件,请重新启动服务让你的服务能够识别你定义的模块,并重启 vscode 让编辑器也能够识别(真的恶心)
  • 设置好你的 tsconfig ,比如记得把 strictPropertyInitialization 设为 false,不然你定义一个变量就必须给它一个初始值。
  • 千万管理好你的路由层级,不然到时连正则都拯救不了你
  • 业务层面千万做好类型检测或者枚举定义,这样不仅便利了开发,还能在出了问题的时候迅速定位
  • 跨模块使用 vuex,请直接使用 rootGetters
  • 如果你需要改造某组件库主题,请单开一个文件进行集中管理,别一个组件分一个文件去改动,不然编译起来速度堪忧
  • 能够复用团队其他人开发好的东西,尽量别去开发第二遍,不然到时浪费的可能就不是单纯的开发时间,还有 code review 的时间

诸如此类的还有一堆,但更多的得你们自己去探寻。接下来,我将谈谈大型项目中团队协作的一些规范

参考文章

六、声明文件

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $jQuery 了。

我们通常这样获取一个 idfoo 的元素:

$('#foo');// orjQuery('#foo');

但是在 ts 中,编译器并不知道 $jQuery 是什么东西1

jQuery('#foo');// ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型2

declare var jQuery: (selector: string) => any;
jQuery('#foo');

上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

jQuery('#foo');

这个就是ts很严格,在只引入script标签(引入全局变量)的情况下不认识$,所以需要声明一下

ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了。

这是全局声明,如果只是在一个文件中声明,那么只是在这个文件中有效

假如仍然无法解析,那么可以检查下 tsconfig.json 中的 filesincludeexclude 配置,确保其包含了 jQuery.d.ts 文件。

这里只演示了全局变量这种模式的声明文件,假如是通过模块导入的方式使用第三方库的话,那么引入声明文件又是另一种方式了,将会在后面详细介绍。

当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped

6.1 全局变量

我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。

@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

npm install @types/jquery --save-dev

可以在这个页面搜索你需要的声明文件。

<script> 标签引入 jQuery,注入全局变量 $jQuery

使用全局变量的声明文件时,如果是以 npm install @types/xxx --save-dev 安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到 src 目录下

6.2 npm包

般我们通过 import foo from 'foo' 导入一个 npm 包,这是符合 ES6 模块规范的。

在我们尝试给一个 npm 包创建声明文件之前,需要先看看它的声明文件是否已经存在。一般来说,npm 包的声明文件可能存在于两个地方:

  1. 与该 npm 包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。

  2. 发布到 @types。我们只需要尝试安装一下对应的 @types 包就知道是否存在该声明文件,安装命令是 npm install @types/foo --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。

假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:

  1. 创建一个 node_modules/@types/foo/index.d.ts 文件,存放 foo 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。

  2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 pathsbaseUrl 字段。

七、内置对象

ECMAScript 标准提供的内置对象有:

BooleanErrorDateRegExp 等。

我们可以在 TypeScript 中将变量定义为这些类型:

let b: Boolean = new Boolean(1);let e: Error = new Error('Error occurred');let d: Date = new Date();let r: RegExp = /[a-z]/;

更多的内置对象,可以查看 MDN 的文档

而他们的定义文件,则在 TypeScript 核心库的定义文件中。

DOM 和 BOM 的内置对象

DOM 和 BOM 提供的内置对象有:

DocumentHTMLElementEventNodeList 等。

TypeScript 中会经常用到这些类型:

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

TypeScript 核心库的定义文件

TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。

当你在使用一些常用的方法的时候,TypeScript 实际上已经帮你做了很多类型判断的工作了,比如: