TypeScript 在Vue项目中的使用总结

新的 VueCLI工具允许开发者 使用 TypeScript 集成环境 创建新项目。只需运行 vue createmy-app。然后,命令行会要求选择预设。使用箭头键选择 Manuallyselectfeatures。接下来,只需确保选择了 TypeScriptBabel选项。

目录会多出两个文件:

  • shims-tsx.d.ts,允许你以 .tsx结尾的文件,在 Vue项目中编写 jsx代码
  • shims-vue.d.ts 主要用于 TypeScript 识别 .vue 文件, Ts默认并不支持导入 vue 文件,这个文件告诉 ts导入 .vue 文件都按 VueConstructor<Vue>处理。

重点还是讲讲 TypeScriptvue-property-decorator

TypescriptJavascript共享相同的基本类型,但有一些额外的类型。

  • 元组 Tuple
  • 枚举 enum
  • AnyVoid

1. 基本类型合集

// 数字,二、八、十六进制都支持
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// 字符串,单双引都行
let name: string = "bob";
let sentence: string = `Hello, my name is ${ name }.`
// 数组,第二种方式是使用数组泛型,Array<元素类型>
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
let u: undefined = undefined;
let n: null = null;
1
2
3
4
5
6
7
8
9
10
11

2. 特殊类型

1. 元组 Tuple

想象 元组 作为有组织的数组,你需要以正确的顺序预定义数据类型。

const messyArray = [' something', 2, true, undefined, null];
const tuple: [number, string, string] = [24, "Indrek" , "Lasn"]
1
2

如果不遵循 为元组 预设排序的索引规则,那么 Typescript会警告。

2. 枚举 enum

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

// 默认情况从0开始为元素编号,也可手动为1开始
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;  // c输出为1

let colorName: string = Color[2];
console.log(colorName);  // 输出'Green'因为上面代码里它的值是2
1
2
3
4
5
6
3. Void

Typescript中,你必须在函数中定义返回类型。我们可以将返回值定义为Void,此时将无法return

4. Any

当你无法确认在处理什么类型时可以用这个,接受所有类型,尽量少用。

5. Never

never类型表示的是那些永不存在的值的类型,用于总是会抛出异常或根本就不会有返回值的函数表达式。

具体的行为是:

  • thrownewError(message)
  • returnerror("Something failed")
  • while(true){}// 存在无法达到的终点

3. 类型断言

可以用来手动指定一个值的类型。

有两种写法,尖括号和 as:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
1
2
3

使用例子有:

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

function getLength(something: string | number): number {    
	return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.//   Property 'length' does not exist on type 'number'.
1
2
3
4

如果你访问长度将会报错,因为如果变量是数字则没有length这个属性,有时候我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,此时需要断言才不会报错,操作如下:

function getLength(something: string | number): number {
    if((<string>something).length) {
        return (<string>something).length
    } else {
        return something.toString().length;
    }
}
1
2
3
4
5
6
7

安全导航操作符 ( ?. )和非空断言操作符(!.)

安全导航操作符 ( ?. ) 和空属性路径: 为了解决导航时变量值为null时,页面运行时出错的问题。

The null hero's name is {{nullHero?.name}}
1

非空断言操作符:

能确定变量值一定不为空时使用。

与安全导航操作符不同的是,非空断言操作符不会防止出现 null 或 undefined。

let s = e!.name;  // 断言e是非空并访问name属性
1

4. 泛型: Generics

支持现有的数据类型和将来添加的数据类型的组件为大型软件系统的开发过程提供很好的灵活性。可以使用"泛型"来创建可复用的组件,并且组件可支持多种数据类型。便可以让用户根据自己的数据类型来使用组件。

1. 泛型方法

在TypeScript里,声明泛型方法有以下两种方式:

function gen_func1<T>(arg: T): T {
    return arg;
}
// 或者
let gen_func2: <T>(arg: T) => T = function (arg) {    
    return arg;
}
1
2
3
4
5
6
7

调用方式也有两种:

// 可以看到通过泛型我将string类型传入保证函数的返回值和函数形参都是string类型
gen_func1<string>('Hello world'); 
gen_func2('Hello world'); 
// 第二种调用方式可省略类型参数,因为编译器会根据传入参数来自动识别对应的类型。
1
2
3
4
2. 泛型与 Any

Ts 的特殊类型 Any 在具体使用时,可以代替任意类型,咋一看两者好像没啥区别,其实不然:

// 方法一:带有any参数的方法
function any_func(arg: any): any {    
    console.log(arg.length);        
    return arg;
}
// 方法二:Array泛型方法
function array_func<T>(arg: Array<T>): Array<T> {      
    console.log(arg.length);        
    return arg;
}
1
2
3
4
5
6
7
8
9
10
  • 方法一,打印了 arg参数的 length属性。因为 any可以代替任意类型,所以该方法在传入参数不是数组或者带有 length属性对象时,会抛出异常。
  • 方法二,定义了参数类型是 Array的泛型类型,肯定会有 length属性,所以不会抛出异常。
3. 泛型类型

泛型接口:

interface Generics_interface<T> {    
    (arg: T): T;
}
function func_demo<T>(arg: T): T {    
    return arg;
}
let func1: Generics_interface<number> = func_demo;
func1(123);     // 正确类型的实际参数
func1('123');   // 错误类型的实际参数
1
2
3
4
5
6
7
8
9

5. 自定义类型: Interface (接口)vs Typealias(类型别名)

1. 相同点

都可以用来描述一个对象或函数:

interface User {  
    name: string  age: number
}
type User = {  
    name: string  age: number
};
interface SetUser {  
    (name: string, age: number): void;
}
type SetUser = (name: string, age: number): void;
1
2
3
4
5
6
7
8
9
10

都允许拓展(extends):

interfacetype 都可以拓展,并且两者并不是相互独立的,也就是说 interface可以 extends type, type 也可以 extends interface虽然效果差不多,但是两者语法不同

interface extends interface

interface Name {
	name: string;
}
interface User extends Name {
	age: number; 
}
1
2
3
4
5
6

type extends type

type Name = {
	name: string; 
}
type User = Name & { age: number  };
1
2
3
4

interface extends type

type Name = {   
    name: string; 
}
interface User extends Name {   
    age: number; 
}
1
2
3
4
5
6

type extends interface

interface Name {   
	name: string; 
}
type User = Name & { age: number; }
1
2
3
4
2. 不同点

type 可以而 interface 不行

  • type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string
// 联合类型
interface Dog {    
    wong();
}
interface Cat {    
    miao();
}
type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
1
2
3
4
5
6
7
8
9
10
11
12
13
  • type 语句中还可以使用 typeof获取实例的 类型进行赋值
// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div;
1
2
3
  • 其他骚操作
type StringOrNumber = string | number;  
type Text = string | { text: string };  
type NameLookup = Dictionary<string, Person>; type Callback<T> = (data: T) => void;  
type Pair<T> = [T, T];  
type Coordinates = Pair<number>;  
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
1
2
3
4
5
6

interface可以而 type不行

interface 能够声明合并

interface User {  name: string  age: number}
interface User {  sex: string}
/*User 接口为 {  name: string  age: number  sex: string }*/
1
2
3

interface 有可选属性和只读属性

  • 可选属性

    接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 例如给函数传入的参数对象中只有部分属性赋值了。带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ?符号。如下所示

interface Person {  
    name: string;  
    age?: number;  
    gender?: number;
}
1
2
3
4
5
  • 只读属性

    顾名思义就是这个属性是不可写的,对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性,如下所示:

interface User {    
    readonly loginName: string;    
    password: string;
}
1
2
3
4

上面的例子说明,当完成User对象的初始化后loginName就不可以修改了。

6.实现与继承: implementsvs extends

extends很明显就是ES6里面的类继承,implement则与C#或Java里接口的基本作用一样, TypeScript也能够用它来明确的强制一个类去符合某种契约。

implement基本用法

interface IDeveloper {
    name: string;
    age?: number;
}
// OK
class dev implements IDeveloper {
    name = 'Test';
    age = 20;
}
// OK
class dev2 implements IDeveloper {
    name = 'Test';
}
// Error
class dev3 implements IDeveloper {
    name = 'Test';
    age = '9';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

extends是继承父类,两者其实可以混着用:

class A extends B implements C,D,E
1

搭配 interfacetype的用法有:

interface Shape {
    area(): number
}
type Perimeter = {
    perimiter(): number
}
class Rectangle implements PoinType, Shape, Perimeter {
    x = 3;
    y = 3;
    area() {
        return this.x * this.y;
    }
    perimiter() {
		return 2 * (this.x + this.y);
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

7.声明文件与命名空间: declarenamespace

Vue项目中的 shims-tsx.d.tsshims-vue.d.ts,其初始内容是这样的:

// shims-tsx.d.tsimport Vue, { VNode } from 'vue';
declare global {  
    namespace JSX {    
        // tslint:disable no-empty-interface    
        interface Element extends VNode {}    
        // tslint:disable no-empty-interface    
        interface ElementClass extends Vue {}    
        interface IntrinsicElements {      
            [elem: string]: any;    
        }  
	}
}
// shims-vue.d.ts
declare module '*.vue' {  
    import Vue from 'vue';  
    export default Vue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

declare:当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

这里列举出几个常用的:

declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare global 扩展全局变量
declare module 扩展模块
1
2
3
4
5
6

namespace:“内部模块”现在称做“命名空间”

module X { 相当于现在推荐的写法 namespace X {)

类似模块,同样也可以通过为其他 JS 库使用了命名空间的库创建 .d.ts 文件的声明文件,如为 D3 JS 库,可以创建这样的声明文件:

declare namespace D3{    
    export interface Selectors { ... }
}
declare var d3: D3.Base;
1
2
3
4

所以上述两个文件:

  • shims-tsx.d.ts, 在全局变量 global中批量命名了数个内部模块。
  • shims-vue.d.ts,意思是告诉 TypeScript *.vue 后缀的文件可以交给 vue 模块来处理。

8. 访问修饰符: privatepublicprotected

其实很好理解:

  1. 默认为 public
  2. 当成员被标记为 private时,它就不能在声明它的类的外部访问,比如:
class Animal {  
	private name: string;
   constructor(theName: string) {    
      this.name = theName;  
    }
}
let a = new Animal('Cat').name; //错误,‘name’是私有的
1
2
3
4
5
6
7

protectedprivate类似,但是, protected成员在派生类中可以访问

class Animal {  
    protected name: string;
    constructor(theName: string) {    
        this.name = theName;  
    }
}
class Rhino extends Animal {    
    constructor() {         
        super('Rhino');   
    }            
    getName() {       
        console.log(this.name) //此处的name就是Animal类中的name   
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

9.Vue中的使用方式

官网推荐有两种,其中Vue.extend或者Vue.component,需要与 mixins 结合使用。在 mixin中定义的方法,不会被 typescript 识别到,这就意味着会出现**丢失代码提示、类型检查、编译报错等问题。**其实还是采用 vue-class-component 装饰器写法会更好:

“@”修饰函数倒不如说是引用、调用它修饰的函数。

举个栗子,下面的一段代码,里面两个函数,没有被调用,也会有输出结果:

test(f){    
	console.log("before ...");    
	f()        
	console.log("after ..."); 
}
@test
func(){    
	console.log("func was called");
}
1
2
3
4
5
6
7
8
9

直接运行,输出结果:

before ...
func was called
after ...
1
2
3

上面代码可以看出来:

  • 只定义了两个函数: testfunc,没有调用它们。
  • 如果没有“@test”,运行应该是没有任何输出的。

但是,解释器读到函数修饰符“@”的时候,后面步骤会是这样:

  1. 去调用 test函数, test函数的入口参数就是那个叫“ func”的函数;
  2. test函数被执行,入口参数的(也就是 func函数)会被调用(执行);

换言之,修饰符带的那个函数的入口参数,就是下面的那个整个的函数。

利用 vue-property-decoratorvuex-class提供的装饰器

vue-property-decorator的装饰器:

  • @Prop
  • @PropSync
  • @Provide
  • @Model
  • @Watch
  • @Inject
  • @Provide
  • @Emit
  • @Component (provided by vue-class-component)
  • Mixins (the helper function named mixins provided by vue-class-component)

vuex-class的装饰器:

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

我们拿原始Vue组件模版来看:

import {componentA,componentB} from '@/components';
export default {    
	components: { componentA, componentB},    		
	props: {    
		propA: { type: Number },    
		propB: { default: 'default value' },    		
		propC: { type: [String, Boolean] },  
	},
	// 组件数据  
	data () {    
        return {      
        message: 'Hello'    
        }  
	},  
	// 计算属性  
	computed: {    
        reversedMessage () {      
            return this.message.split('').reverse().join('')    
        }    
        // Vuex数据    
        step() {        
            return this.$store.state.count    
        }  
	},  
	methods: {    
        changeMessage () {      
        	this.message = "Good bye"    
        },    
        getName() {        
        	let name = this.$store.getters['person/name']        
        	return name    
        } 
    },  
	// 生命周期  
	created () { },  
	mounted () { },  
	updated () { },  
	destroyed () { }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

以上模版替换成修饰符写法则是:

import { Component, Vue, Prop } from 'vue-property-decorator';
import { State, Getter } from 'vuex-class';
import { count, name } from '@/person';
import { componentA, componentB } from '@/components';
@Component({    
	components:{ componentA, componentB},
})
export default class HelloWorld extends Vue{    
	@Prop(Number) readonly propA!: number | undefined  
	@Prop({ default: 'default value' }) readonly propB!: string  
	@Prop([String, Boolean]) readonly propC!: string | boolean | undefined
  	// 原data  
  	message = 'Hello'
  	// 计算属性    
  	private get reversedMessage (): string[] {      
  		return this.message.split('').reverse().join('')  
  	}  
  	// Vuex 数据  
  	@State((state: IRootState) => state . booking. currentStep) step!: number    		@Getter( 'person/name') name!: name
  	// method  
  	public changeMessage (): void {    this.message = 'Good bye'  },  
  	public getName(): string {    let storeName = name;   return storeName  }    
  	// 生命周期  
  	private created ()void { },  
  	private mounted ()void { },  
  	private updated ()void { },  
  	private destroyed ()void { }
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

正如你所看到的,我们在生命周期 列表那都添加 private XXXX方法,因为这不应该公开给其他组件。

而不对 method做私有约束的原因是,可能会用到 @Emit来向父组件传递信息。

添加全局工具多语言i18n

引入全局模块,需要改 main.ts:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

new Vue({  
    router,  
    store,  
    render: (h) => h(App),
}).$mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12

npm i VueI18n

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 新模块
import i18n from './i18n';
Vue.config.productionTip = false;
new Vue({    
    router,     
    store,     
    i18n, // 新模块    
    render: (h) => h(App),
}).$mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13

但仅仅这样,还不够。你需要动 src/vue-shim.d.ts

// 声明全局方法
declare module 'vue/types/vue' {  
    interface Vue {        
        readonly $i18n: VueI18Next;        
        $t: TranslationFunction;    
    }
}
1
2
3
4
5
6
7

之后使用 this.$i18n()的话就不会报错了。

Axios 使用与封装
  • 新建文件 request.ts

文件目录:

-api    
	- main.ts   // 实际调用
-utils    
	- request.ts  // 接口封装
1
2
3
4
  • request.ts文件解析
import * as axios from 'axios';
import store from '@/store';
// 这里可根据具体使用的UI组件库进行替换
import { Toast } from 'vant';
import { AxiosResponse, AxiosRequestConfig } from 'axios';

 /* baseURL 按实际项目来定义 */
 const baseURL = process.env.VUE_APP_URL;
 /* 创建axios实例 */
 const service = axios.default.create({    
     baseURL,    
     timeout: 0, // 请求超时时间    
     maxContentLength: 4000,
 });
service.interceptors.request.use((config: AxiosRequestConfig) => {    
	return config;
}, (error: any) => {    
	Promise.reject(error);
});
service.interceptors.response.use(    
	(response: AxiosResponse) => {        
		if (response.status !== 200) {            
			Toast.fail('请求错误!');        
		} else {            
			return response.data;        
		}   
	}, (error: any) => {        
		return Promise.reject(error);    
});
export default service;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

为了方便,我们还需要定义一套固定的 axios 返回的格式,新建 ajax.ts

export interface AjaxResponse {    
    code: number;    
    data: any;    
    message: string;
}
1
2
3
4
5
  • main.ts接口调用:
// api/main.ts
import request from '../utils/request';
// get
export function getSomeThings(params:any) {    
	return request({        
		url: '/api/getSomethings',   
	});
}
// post
export function postSomeThings(params:any) {    
	return request({        
		url: '/api/postSomethings',        
		methods: 'post',        
		data: params   
	});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
编写一个组件

为了减少时间,我们来替换掉 src/components/HelloWorld.vue,做一个博客帖子组件:

<template>    
    <div class="blogpost">        
        <h2>{{ post.title }}</h2>        
		<p>{{ post.body }}</p>        
		<p class="meta">Written by {{ post.author }} on {{ date }}</p>    
	</div>
</template>
<script lang="ts">
    import { Component, Prop, Vue } from 'vue-property-decorator';
	// 在这里对数据进行类型约束
	export interface Post {    
        title: string;    
        body: string;    
        author: string;    
        datePosted: Date;
    }
	@Componentexport default class HelloWorld extends Vue {    
		@Prop() private post!: Post;
    	get date() {       	   return`${this.post.datePosted.getDate()}/${this.post.datePosted.getMonth()}/${this.post.datePosted.getFullYear()}`;    
        }}
</script>
<style scoped>
    h2 {  
        text-decoration: underline;
    }
	p.meta {  
        font-style: italic;
    }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

然后在 Home.vue中使用:

<template>
	<div class="home" > 
        <img alt="Vue logo" src = "../assets/logo.png" >
		<HelloWorld v-for= "blogPost in blogPosts" :post = "blogPost" :key = "blogPost.title" />
	</div>
</template>
< script lang = "ts" >
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld, { Post } from '@/components/HelloWorld.vue'; 
// @ is an alias to /src
@Component({ components: { HelloWorld, }, })
export default class Home extends Vue { 
	private blogPosts: Post[] = [
        { 
            title: 'My first blogpost ever!', 
            body: 'Lorem ipsum dolor sit amet.', 
            author: 'Elke', 
            datePosted: new Date(2019, 1, 18), 
        }, 
		{ 
            title: 'Look I am blogging!', 
            body: 'Hurray for me, this is my second post!',
            author: 'Elke', 
            datePosted: new Date(2019, 1, 19), 
        }, 
		{ 
            title: 'Another one?!', 
            body: 'Another one!', 
            author: 'Elke', 
            datePosted: new Date(2019, 1, 20), 
        },]; 
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

参考

1.Vue 3.0前的 TypeScript 最佳入门实践

2.Vue的TS支持