在上一篇ts基础篇 中,我们介绍了ts的基础类型和如何定义了数组对象函数等;在这一TypeScript进阶篇,我们来介绍TS的高级用法,比如泛型和在项目中如何进行配置以及使用。
声明文件 当使用一些第三方库时,有一些通过script标签引入的全局变量,TypeScript会出现识别不到而报错的情况,我们需要对其进行声明,这些声明就需要写到声明文件中。比如我们在项目中使用jQuery,在全局使用变量$
或jQuery
:
$('#foo' );jQuery ('#foo' );
我们就需要将jQuery的声明语句放到单独的文件中,这就是声明文件:
declare let $ : (selector: string ) => any ;declare let jQuery : (selector: string ) => any ;
一般ts会解析项目src文件夹下的所有.ts
文件,因此也会解析.d.ts
文件,这样所有的ts文件就会得到jQuery的类型定义了。
加载社区声明文件 当然,jQuery的声明文件,社区已经写好了,不需要我们自己来定义;我们可以使用@types来管理声明文件:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
npm install @types/jquery --save-dev
通过配置tsconfig.json
,将声明文件引入:
{ "compilerOptions" : { "types" : [ "jquery" ] } }
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
编写自己的声明文件 声明文件的语法主要有下面几种:
declare let 和 declare const 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare namespace 声明(含有子属性的)全局对象
interface 和 type 声明全局类型
declare let
和 declare const
声明是最简单的,用来声明一个全局变量类型;let定义的全局变量允许修改,而const定义的则不允许修改
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
declare let $ : (selector: string ) => any ;declare const jQuery : (selector: string ) => any ; $ = function ( ){} jQuery = function ( ){}
一般来说,声明的全局变量都是禁止修改的常量,所以大部分的情况都应该使用declare const
进行声明。同时需要注意的是,声明语句中只能定义类型,而不能定义具体的实现代码。
declare function
用来定义全局函数的类型,jQuery是一个函数,因此我们也可以通过函数的方式来进行定义:
declare function jQuery (selector: string ): any ;
在函数声明中也能够支持函数重载:
declare function jQuery (selector: string ): any ;declare function jQuery (domReadyCallback: () => any ): any ;
declare class
用来声明一个全局类:
declare class Animal { name : string ; constructor (name: string ); sayHi (): string ; }let cat = new Animal ('Tom' );
declare enum
用来声明全局枚举类型:
declare enum Directions { Up , Down , Left , Right }
declare namespace
用来声明含有子属性的全局对象(模块)。刚开始ts使用module
关键字来表示内部的模块,但随着ES6也使用了module
关键字,ts为了兼容ES6,从1.5版本开始将module
改名为namespace
;比如jQuery是一个全局变量对象,它上面挂载了很多的方法可以调用,我们就通过namespace
来进行声明:
declare namespace jQuery { function ajax (url: string , settings?: any ): void ; }
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
在jQuery内部,我们还可以使用const、class、enum等语句进行声明:
declare namespace jQuery { function ajax (url: string , settings?: any ): void ; const version : number ; class Event { blur (eventType : EventType ): void } enum EventType { CustomClick } }
同时,如果需要声明的对象层级较深,我们还可以使用namespace
进行嵌套声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 declare namespace jQuery { function ajax (url: string , settings?: any ): void ; namespace fn { function extend (object : any ): void ; } } jQuery.ajax ('/api/get' ); jQuery.fn .extend ({ check : function ( ) { return this .each (function ( ) { this .checked = true ; }); } });
泛型 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性;简单来说,就是一种创建可复用代码组件的工具,这种组件不止能被一种类型使用,而是能够被多种类型进行复用。
简单的泛型例子 我们来实现一个重复元素功能的函数,将给定的元素重复给定的次数,最后返回一个数组:
function repeatArray (value: string , length: number ): Array <string > { let list = []; for (let i = 0 ; i < length; i++) { list.push (value); } return list; }repeatArray ("5" , 3 );
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们接收了string类型,并且返回string类型的数组;但是这显得太死板了,因为我们只能接收string类型,如果我们想传入number或者object都会报错。那如果改成any
呢?
function repeatArray (value: any , length: number ): Array <any > { let list = []; for (let i = 0 ; i < length; i++) { list.push (value); } return list; }
使用any会导致这个函数可以接收任意类型的参数,这样就导致这个函数缺乏了有效的信息提示,不能告诉函数的调用者传入类型和返回数组中的类型应该是相同的;假设我们传入一个数字,我们只能知道任何类型的值都有可能被返回。
这样我们就需要通过泛型来定义这个函数:
function repeatArray<T>(value : T, length : number ): Array <T> { let list : T[] = []; for (let i = 0 ; i < length; i++) { list.push (value); } return list; }
我们在函数名后面添加了类型变量<T>
,T
用来指代任意输入的类型,在后面的输入参数的类型和输出函数的类型中都可以使用。
需要注意的是,这里的字母T
只是代表了一个变量,在数学中和x、y的性质是一样的;我们还可以用其他的参数,比如用S、U、T、Y等其他字母来替代。
定义泛型函数后,我们可以用两种方式来调用,第一种,传入所有的参数,包含类型参数:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
let list1 = repeatArray<string >("5" , 3 );let list2 = repeatArray<number >(6 , 5 );
第二种方式,利用类型推论,让编译器自动确定类型变量的类型:
let list1 = repeatArray ("5" , 3 );let list2 = repeatArray (6 , 5 );
多个类型参数 在定义函数时,我们有可能会用到多个泛型变量,,用逗号分隔这多个变量:
function swap<T, U>(tuple : [T, U]): [U, T] { return [tuple[1 ], tuple[0 ]]; }swap ([7 , 'seven' ]);
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
在swap函数中,我们通过2个变量来交换输入的数组中的元素
泛型类 我们不仅用泛型定义函数,还可以用泛型定义一个类,和函数类似,也是通过<T>
跟在类名后面:
class Animal <T> { name : T; constructor (name: T ) { this .name = name; } sayName (): T { return this .name ; } }let dog = new Animal ("tom" ); dog.sayName ()
泛型约束 在函数内部,如果需要使用泛型变量上的属性,由于不知道它的类型(等同于Unknow类型),因此不能随意调用属性和方法:
function showLength<T>(arg : T): T { console .log (arg.length ); return arg; }
泛型变量T不一定有属性length,因此会报错;我们可以对泛型变量进行约束,只允许传入包含length属性的变量:
interface Lengthwise { length : number ; }function showLength<T extends Lengthwise >(arg : T): T { console .log (arg.length ); return arg; }showLength ('123' )showLength ([])showLength ({ length : 3 , value : 4 })showLength (5 )
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
另外,多个泛型参数之间也可以互相约束:
function copy<T extends U, U>(target : T, source : U): T { for (let id in source) { target[id] = (<T>source)[id]; } return target; }copy ({ a : 1 , b : 2 , c : 3 }, { a : 1 , b : 2 });
我们将source上所有的属性拷贝到target上,通过T extends U
,保证了source上所有的属性在target上都有。
泛型接口 在ts基础篇 中,我们通过接口来定义了函数表达式:
interface ISumFunc { (x : number , y : number ): number ; }let sum : ISumFunc = function (x, y ) { return x + y; };
同时可以使用含有泛型的接口来约束函数:
interface CreateArrayFn { <T>(length : number , item : T): Array <T>; }let createList : CreateArrayFn ; createList = function <T>(len : number , item : T): Array <T> { let list : T[] = []; for (let i = 0 ; i < len; i++) { list.push (item); } return list; };createList (3 , "4" );createList (3 , 5 );
项目配置 说了这么多ts的知识,我们来把他结合到项目中进行使用和配置。
tsconfig.json配置文件详解 tsconfig.json是ts编译器的配置文件,ts编译器可根据它的信息来对代码进行编译;运行tsc,它会在当前目录或者父级目录寻找配置文件。在配置文件中可以通过compilerOptions
来定制我们的编译选项:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 { "compilerOptions" : { "target" : "es5" , "module" : "commonjs" , "lib" : [ ] , "allowJs" : true , "checkJs" : true , "jsx" : "preserve" , "declaration" : true , "sourceMap" : true , "outFile" : "./" , "outDir" : "./" , "rootDir" : "./" , "removeComments" : true , "noEmit" : true , "importHelpers" : true , "isolatedModules" : true , "strict" : true , "noImplicitAny" : true , "strictNullChecks" : true , "noImplicitThis" : true , "alwaysStrict" : true , "noUnusedLocals" : true , "noUnusedParameters" : true , "noImplicitReturns" : true , "noFallthroughCasesInSwitch" : true , "moduleResolution" : "node" , "baseUrl" : "./" , "paths" : { } , "rootDirs" : [ ] , "typeRoots" : [ ] , "types" : [ ] , "allowSyntheticDefaultImports" : true , "sourceRoot" : "./" , "mapRoot" : "./" , "inlineSourceMap" : true , "inlineSources" : true , "experimentalDecorators" : true , "emitDecoratorMetadata" : true } }
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
也可以通过files
显式指定需要编译的文件:
{ "files" : [ "./some/file.ts" ] }
还可以使用include
和exclude
选项来指定需要包含的文件和排除的文件:
{ "include" : [ "src/**/*.ts" , "src/**/*.tsx" , "src/**/*.vue" , "tests/**/*.ts" , "tests/**/*.tsx" ] , "exclude" : [ "node_modules" ] }
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
include
和exclude
支持的glob通配符有:
*
匹配0或多个字符(不包括目录分隔符)
?
匹配一个任意字符(不包括目录分隔符)
**/
递归匹配任意子目录
在ts中使用ESLint和Prettier 有些童鞋可能会有疑惑了,ts在编译阶段就能排查出代码错误,为什么还需要用到eslint来检查呢?因为ts重点关注的是类型的检查,而不是代码和风格的检查,有一些代码的问题,比如==与===的检查、禁用var等功能,还是需要eslint来配合;首先在项目中安装eslint的依赖:
npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
这三个依赖的作用分别是:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
eslint: ESLint的核心代码
@typescript-eslint/parser:ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码
@typescript-eslint/eslint-plugin:这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范
安装依赖后我们就可以在.eslintrc.js中配置插件:
module .exports = { parser : '@typescript-eslint/parser' , extends : ['plugin:@typescript-eslint/recommended' ], plugins : ['@typescript-eslint' ], env :{ browser : true , node : true , } }
在一文彻底读懂ESLint 中还介绍了Eslint配合了Prettier,在ts项目,我们也可以搭配Prettier来格式化代码,首先也是进行安装:
npm i -g prettier eslint-config-prettier eslint-plugin-prettier
prettier:prettier插件的核心代码
eslint-config-prettier:解决ESLint中的样式规范和prettier中样式规范的冲突,以prettier的样式规范为准,使ESLint中的样式规范自动失效
eslint-plugin-prettier:将prettier作为ESLint规范来使用
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
然后还是在.eslintrc.js配置Prettier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module .exports = { parser : '@typescript-eslint/parser' , extends :[ 'prettier/@typescript-eslint' , 'plugin:prettier/recommended' ], parserOptions : { "ecmaVersion" : 2019 , "sourceType" : 'module' , "ecmaFeatures" :{ jsx :true } }, env :{ browser : true , node : true , } }
在vue中使用ts 在vue中使用ts,推荐使用基于类的注解装饰器
进行开发,vue官方推荐vue-class-component
插件,但是我们在实际开发中都会用到vue-class-component
这个插件,也是vue社区推荐的;它是基于vue-class-component
开发而成,但是性能上有一些改进;他具备以下几个装饰器和功能:
我们来看下每个装饰器的用法:
@Component @Component装饰器接口一个对象做参数,可以在对象中声明components
,filters
,directives
等装饰器的选项,也可以声明computed,watch等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div> <div>{{ firtName | filterName }}</div> <HelloWorld></HelloWorld> </div> </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; @Component({ components: { HelloWorld, }, filters: { filterName(val: string) { return val + ":filter name"; }, }, }) export default class Home extends Vue { private firtName = "tom"; } </script>
除了上面介绍的属性,还可以注册钩子函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script lang="ts"> Component.registerHooks([ "beforeRouteLeave", "beforeRouteEnter", ]); @Component export default class Home extends Vue { beforeRouteLeave(to: any, from: any, next: any) { console.log('beforeRouteLeave'); next(); } beforeRouteEnter(to: any, from: any, next: any) { console.log('beforeRouteLeave'); next(); } } </script>
@Prop @Prop装饰器同vue中props功能相同,接收一个参数,这个参数可以有三种写法:
<script lang="ts"> @Component export default class Home extends Vue { @Prop(String) readonly name!: string | undefined; @Prop({ default: 30, type: Number }) private age!: number; @Prop([String, Boolean]) private sex!: string | boolean; } </script>
需要注意的是:属性的ts类型后面需要加上undefined类型;或者在属性名后面加上!,表示非null 和 非undefined 的断言,否则编译器会给出错误提示。
@PropSync @PropSync装饰器与@prop用法类似,二者的区别在于:
@PropSync本质上就是通过vue的sync方式传参:
<template> <h3 @click="changeMsg">{{ msg }}</h3> </template> <script lang="ts"> import { Component,PropSync, Vue } from "vue-property-decorator"; @Component export default class HelloWorld extends Vue { @PropSync("msg") msgSync!: string; changeMsg(): void { this.msgSync = "new msg"; } } </script>
@Watch @Watch装饰器同vue中的watch功能相同,监听依赖的变量值变化而做一系列操作,它接收两个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div> <h1>child:{{child}}</h1> <input type="text" v-model="child"/> </div> </template> <script lang="ts"> import { Vue, Watch, Component } from 'vue-property-decorator'; @Component export default class Home extends Vue { private child = ''; @Watch('child', { immediate: false, deep: false }) onChildChanged(newValue: string, oldValue: string) { console.log(newValue); console.log(oldValue); } } </script>
@Emit @Emit同vue中的$emit,它接收一个可选参数,该参数是$emit的第一个参数,充当事件名;如果没有提供这个参数,$Emit会将回调函数名的camelCase转为kebab-case,并将其作为事件名。
<script lang="ts"> export default class Home extends Vue { @Emit() clickBtn() { } @Emit('click-my-btn') clickBtn1() { } } </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
最后相当于以下代码:
<script> export default { clickBtn(){ this.$emit('click-btn') } clickBtn1(){ this.$emit('click-my-btn') } } </script>
@Emit会将回调函数的返回值作为第二个参数返回给父级函数,如果没有返回值,则会默认使用括号里的参数:
<script lang="ts"> export default class Home extends Vue { @Emit() returnVal() { return 'hello parent' } @Emit() clickBtn(ev) { } } </script>
等同于以下代码:
<script> export default { returnVal(){ this.$emit('return-val', 'hello parent') } clickBtn(ev){ this.$emit('click-btn', ev) } } </script>
@Model @Model装饰器允许我们在一个组件上自定义v-model,它接收两个参数:
event: string 事件名。
options: Constructor | Constructor[] | PropOptions 与@Prop的第一个参数一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <div> <div>v-model的值: {{ val }}</div> <input type="text" :value="val" @input="changeInput" /> </div> </template> <script lang="ts"> import { Component, Emit, Model, Vue } from "vue-property-decorator"; @Component export default class Input extends Vue { @Model("change", { type: String }) readonly val!: string; @Emit("change") changeInput(ev: any) { return ev.target.value; } } </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们将父组件接收的value值作为变量val,将接收的input函数改名change,在输入框改变时触发了change函数(也就是input函数)。
@Ref @Ref同vue中的$ref,接收一个可选字符串,用来指向元素或子组件的引用信息;如果没有这个参数,则使用装饰器后面的属性名:
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 <template> <div> <div ref="refDiv">{{ fullName | filterName }}</div> <SubComponent ref="subComponent"></SubComponent> <div @click="clickSubmit">submit</div> </div> </template> <script lang="ts"> import SubComponent from "@/components/SubComponent" @Component({ components: { SubComponent, }, }) export default class Home extends Vue { @Ref() readonly refDiv!: HTMLElement; @Ref("refDiv") readonly newRef!: HTMLElement; @Ref() readonly subComponent!: SubComponent; @Ref("subComponent") readonly compRef!: SubComponent; clickSubmit(): void { console.log(this.refDiv); console.log(this.newRef); console.log(this.subComponent); console.log(this.compRef); } } </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里