TypeScript
资源
- TypeScript参考
- Vue+TypeScript
- 代码分支:web22-ts
知识点
- ts核心语法
- ts+vue
- 装饰器应用
- 装饰器原理
- vue-property-decorator源码解析
准备工作
新建一个基于ts的vue项目
在已存在项目中安装typescript
vue add @vue/typescript

请暂时忽略引发的几处Error,它们不会影响项目运行,我们将在后面处理它们。
TS特点
- 类型注解、类型检测
- 类
- 接口
- 泛型
- 装饰器
- 类型声明
类型注解和编译时类型检查
使用类型注解约束变量类型,编译器可以做静态类型检查,使程序更加健壮
类型基础
// ts-test.tslet var1: string; // 类型注解var1 = "开课吧"; // 正确var1 = 4 ; // 错误// 编译器类型推断可省略这个语法let var2 = true;// 常⻅原始类型: string,number,boolean,undefined,null,symbol// 类型数组let arr: string[];arr = ['Tom']; // 或Array<string>// 任意类型anylet varAny: any;varAny = 'xx';varAny = 3 ;// any类型也可用于数组let arrAny: any[];arrAny = [ 1 , true, "free"];arrAny[ 1 ] = 100 ;// 函数中的类型约束function greet(person: string): string {return 'hello, ' + person;}// void类型,常用于没有返回值的函数function warn(): void {}
范例,HelloWorld.vue
<template><div><ul><li v-for="feature in features" :key="feature">{{feature}}</li></ul></div></template><script lang='ts'>import { Component, Prop, Vue } from "vue-property-decorator";@Componentexport default class Hello extends Vue {features: string[] = ["类型注解", "编译型语言"];}</script>
类型别名
使用类型别名自定义类型
// 可以用下面这样方式定义对象类型const objType: { foo: string, bar: string }// 使用type定义类型别名,使用更便捷,还能复用type Foobar = { foo: string, bar: string }const aliasType: Foobar
范例:使用类型别名定义Feature,types/index.ts
export type Feature = {id: number,name: string}
使用自定义类型,HelloWorld.vue
<template><div><!--修改模板--><li v-for="feature in features" :key="feature.id">{{feature.name}}</li></div></template><script lang='ts'>// 导入接口import { Feature } from "@/types";@Componentexport default class Hello extends Vue {// 修改数据结构features: Feature[] = [{ id: 1, name: "类型注解" }];}</script>
联合类型
希望某个变量或参数的类型是多种类型其中之一
let union: string | number;union = '1'; // okunion = 1 ; // ok
交叉类型
想要定义某种由多种类型合并而成的类型使用交叉类型
type First = {first: number};type Second = {second: number};// FirstAndSecond将同时拥有属性first和secondtype FirstAndSecond = First & Second;
范例:利用交叉类型给Feature添加一个selected属性
// types/index.tstype Select = {selected: boolean}export type FeatureSelect = Feature & Select
使用这个FeatureSelect,HelloWorld.vue
features: FeatureSelect[] = [{ id: 1 , name: "类型注解", selected: false },{ id: 2 , name: "编译型语言", selected: true }];<li :class="{selected: feature.selected}">{{feature.name}}</li>.selected {background-color: rgb( 168 , 212 , 247 );}
函数
必填参:参数一旦声明,就要求传递,且类型需符合
// 02-function.tsfunction greeting(person: string): string {return "Hello, " + person;}greeting('tom')
可选参数:参数名后面加上问号,变成可选参数
function greeting(person: string, msg?: string): string {return "Hello, " + person;}
默认值
function greeting(person: string, msg = ''): string {return "Hello, " + person;}
*函数重载:以参数数量或类型区分多个同名函数
// 重载 1function watch (cb1: () => void): void;// 重载 2function watch (cb1: () => void, cb2: (v1: any, v2: any) => void): void;// 实现function watch (cb1: () => void, cb2?: (v1: any, v2: any) => void) {if (cb1 && cb2) {console.log('执行watch重载2');} else {console.log('执行watch重载1');}}
范例:新增特性,Hello.vue
<div><input type="text" placeholder="输入新特性" @keyup.enter="addFeature"></div>
addFeature(e: KeyboardEvent) {// e.target是EventTarget类型,需要断言为HTMLInputElementconst inp = e.target as HTMLInputElement;const feature: FeatureSelect = {id: this.features.length + 1,name: inp.value,selected: false}this.features.push(feature);inp.value = "";}
范例:生命周期钩子,Hello.vue
created() {this.features = [{ id: 1 , name: "类型注解" }];}
类
class的特性
ts中的类和es6中大体相同,这里重点关注ts带来的访问控制等特性
// 03-class.tsclass Parent {private _foo = "foo"; // 私有属性,不能在类的外部访问protected bar = "bar"; // 保护属性,可以在子类中访问// 参数属性:构造函数参数加修饰符,能够定义为成员属性constructor (public tua = "tua") { }// 方法也有修饰符private someMethod () { }// 存取器:属性方式访问,可添加额外逻辑,控制读写性get foo () {return this._foo;}set foo (val) {this._foo = val;}}
范例:利用getter设置计算属性,Hello.vue
<template><li>特性数量:{{count}}</li></template><script lang="ts">export default class HelloWorld extends Vue {// 定义getter作为计算属性get count () {return this.features.length;}}</script>
接口
接口仅约束结构,不要求实现,使用更简单
// 04-interface// Person接口定义了解构interface Person {firstName: string;lastName: string;}// greeting函数通过Person接口约束参数解构function greeting (person: Person) {return 'Hello, ' + person.firstName + ' ' + person.lastName;}greeting({ firstName: 'Jane', lastName: 'User' }); // 正确greeting({ firstName: 'Jane' }); // 错误
范例:Feature也可用接口形式约束,./types/index.ts
// 接口中只需定义结构,不需要初始化export interface Feature {id: number;name: string;}
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定
类型的一种特性。以此 增加代码通用性 。
// 不用泛型// interface Result {// ok: 0 | 1;// data: Feature[];// }// 使用泛型interface Result<T> {ok: 0 | 1;data: T;}// 泛型方法function getResult<T> (data: T): Result<T> {return { ok: 1, data };}// 用尖括号方式指定T为stringgetResult < string > ('hello')// 用类型推断指定T为numbergetResult(1)
泛型优点:
- 函数和类可以支持多种类型,更加通用
- 不必编写多条重载,冗⻓联合类型,可读性好
- 灵活控制类型约束
不仅通用且能灵活控制,泛型被广泛用于通用库的编写。
范例:用axios获取数据
安装axios:npm i axios -S
配置一个模拟接口,vue.config.js
module.exports = {devServer: {before (app) {app.get('/api/list', (req, res) => {res.json([{ id: 1, name: "类型注解", version: "2.0" },{ id: 2, name: "编译型语言", version: "1.0" }])})}}}
使用接口,HelloWorld.vue
async mounted() {console.log("HelloWorld");const resp = await axios.get < FeatureSelect[] > ('/api/list')this.features = resp.data}
声明文件
使用ts开发时如果要使用 第三方js库 的同时还想利用ts诸如类型检查等特性就需要声明文件,类似xx.d.ts
同时,vue项目中还可以在shims-vue.d.ts中对已存在 模块进行补充
npm i @types/xxx
范例:利用模块补充$axios属性到Vue实例,从而在组件里面直接用
// main.tsimport axios from 'axios'Vue.prototype.$axios = axios;
// shims-vue.d.tsimport Vue from "vue";import { AxiosInstance } from "axios";declare module "vue/types/vue" {interface Vue {$axios: AxiosInstance;}}
范例:给krouter/index.js编写声明文件,index.d.ts
import VueRouter from "vue-router";declare const router: VueRouterexport default router
装饰器
装饰器用于扩展类或者它的属性和方法。@xxx就是装饰器的写法
属性声明:@Prop
除了在@Component中声明,还可以采用@Prop的方式声明组件属性
export default class HelloWorld extends Vue {// Props()参数是为vue提供属性选项// !称为明确赋值断言,它是提供给ts的@Prop({type: String, required: true})private msg!: string;}
事件处理:@Emit
新增特性时派发事件通知,Hello.vue
// 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式)@Emit()private addFeature(event: any) {// 若没有返回值形参将作为事件参数const feature = { name: event.target.value, id: this.features.length + 1 };this.features.push(feature);event.target.value = "";return feature;// 若有返回值则返回值作为事件参数}
变更监测:@Watch
@Watch('msg')onMsgChange(val:string, oldVal:any){console.log(val, oldVal);}
状态管理推荐使用:vuex-module-decorators
vuex-module-decorators通过装饰器提供模块化声明vuex模块的方法,可以有效利用ts的类型系统。
安装
npm i vuex-module-decorators -D
根模块清空,修改store/index.ts
export default new Vuex.Store({})
定义counter模块,创建store/counter
import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators'import store from './index'// 动态注册模块@Module({ dynamic: true, store: store, name: 'counter', namespaced: true })class CounterModule extends VuexModule {count = 1@Mutationadd () {// 通过this直接访问countthis.count++}// 定义gettersget doubleCount () {return this.count * 2;}@ActionasyncAdd () {setTimeout(() => {// 通过this直接访问addthis.add()}, 1000);}}// 导出模块应该是getModule的结果export default getModule(CounterModule)
使用,App.vue
<p @click="add">{{$store.state.counter.count}}</p><p @click="asyncAdd">{{count}}</p>
import CounterModule from '@/store/counter'@Componentexport default class App extends Vue {get count () {return CounterModule.count}add () {CounterModule.add()}asyncAdd () {CounterModule.asyncAdd()}}
装饰器原理
装饰器是 工厂函数 ,它能访问和修改装饰目标。
类装饰器,07-decorator.ts
//类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。function log (target: Function) {// target是构造函数target.prototype.log = function () {console.log(this.bar);}}@logclass Foo {bar = 'bar'}const foo = new Foo();// @ts-ignorefoo.log();
方法装饰器
function rec (target: any, name: string, descriptor: any) {// 这里通过修改descriptor.value扩展了bar方法const baz = descriptor.value;descriptor.value = function (val: string) {console.log('run method', name);baz.call(this, val);}}class Foo {@recsetBar (val: string) {this.bar = val}}foo.setBar('lalala')
属性装饰器
// 属性装饰器function mua (target, name) {target[name] = 'mua~~~'}class Foo {@mua ns!: string;}console.log(foo.ns);
稍微改造一下使其可以接收参数
function mua (param: string) {return function (target, name) {target[name] = param}}
实战一下:实现Component装饰器
<template><div>{{msg}}</div></template><script lang='ts'>import { Vue } from "vue-property-decorator";function Component (options: any) {return function (target: any) {return Vue.extend(options);};}@Component({props: {msg: {type: String,default: ""}}})export default class Decor extends Vue { }</script>
显然options中的选项都可以从目标组件定义中找到:
function Component (target: any): any {const option: any = {};const proto = target.prototype;const keys = Object.getOwnPropertyNames(proto);keys.forEach((key) => {if (key !== "constructor") {const descriptor = Object.getOwnPropertyDescriptor(proto, key);if (descriptor) {if (typeof descriptor.value === "function") {if (["created", "mounted"].indexOf(key) !== - 1) {option[key] = proto[key];} else {const methods = (!option.methods || (option.methods = {})) as any;methods[key] = descriptor.value;}} else if (descriptor.get || descriptor.set) {const computed = (!option.computed || (option.computed = {})) as any;computed[key] = {get: descriptor.get,set: descriptor.set,};}}}});option.data = function () {const vm = new target();const data: any = {};Object.keys(vm).filter((key) => !key.startsWith("_") && !key.startsWith("$")).forEach((key) => {data[key] = vm[key];});return data;};return Vue.extend(option);}
作业
写一个装饰器
思考题
- 把手头的小项目改造为ts编写
- 探究vue-property-decorator中各装饰器实现原理,能造个轮子更佳
