TypeScript从平凡到不凡(进阶篇)

  在上一篇ts基础篇中,我们介绍了ts的基础类型和如何定义了数组对象函数等;在这一TypeScript进阶篇,我们来介绍TS的高级用法,比如泛型和在项目中如何进行配置以及使用。

声明文件

  当使用一些第三方库时,有一些通过script标签引入的全局变量,TypeScript会出现识别不到而报错的情况,我们需要对其进行声明,这些声明就需要写到声明文件中。比如我们在项目中使用jQuery,在全局使用变量$jQuery

1
2
3
4
5
$('#foo');
// or
jQuery('#foo');
// 报错:
// Cannot find name '$'. Do you need to install type definitions for jQuery? Try `npm i --save-dev @types/jquery` and then add `jquery` to the types field in your tsconfig.

  我们就需要将jQuery的声明语句放到单独的文件中,这就是声明文件:

1
2
3
// src/jQuery.d.ts
declare let $: (selector: string) => any;
declare let jQuery: (selector: string) => any;

  一般ts会解析项目src文件夹下的所有.ts文件,因此也会解析.d.ts文件,这样所有的ts文件就会得到jQuery的类型定义了。

加载社区声明文件

  当然,jQuery的声明文件,社区已经写好了,不需要我们自己来定义;我们可以使用@types来管理声明文件:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
npm install @types/jquery --save-dev

  通过配置tsconfig.json,将声明文件引入:

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"types" : [
// 其他配置
"jquery"
]
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

编写自己的声明文件

  声明文件的语法主要有下面几种:

  • declare let 和 declare const 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interface 和 type 声明全局类型

  declare letdeclare const声明是最简单的,用来声明一个全局变量类型;let定义的全局变量允许修改,而const定义的则不允许修改

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
// src/jQuery.d.ts
declare let $: (selector: string) => any;
declare const jQuery: (selector: string) => any;

// src/main.js
$ = function(){}
// 报错
// ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property.
jQuery = function(){}

  一般来说,声明的全局变量都是禁止修改的常量,所以大部分的情况都应该使用declare const进行声明。同时需要注意的是,声明语句中只能定义类型,而不能定义具体的实现代码。

  declare function用来定义全局函数的类型,jQuery是一个函数,因此我们也可以通过函数的方式来进行定义:

1
2
// src/jQuery.d.ts
declare function jQuery(selector: string): any;

  在函数声明中也能够支持函数重载:

1
2
3
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;

  declare class用来声明一个全局类:

1
2
3
4
5
6
7
8
9
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}

// src/index.ts
let cat = new Animal('Tom');

  declare enum用来声明全局枚举类型:

1
2
3
4
5
6
7
8
// src/Directions.d.ts

declare enum Directions {
Up,
Down,
Left,
Right
}

  declare namespace用来声明含有子属性的全局对象(模块)。刚开始ts使用module关键字来表示内部的模块,但随着ES6也使用了module关键字,ts为了兼容ES6,从1.5版本开始将module改名为namespace;比如jQuery是一个全局变量对象,它上面挂载了很多的方法可以调用,我们就通过namespace来进行声明:

1
2
3
4
5
// src/jQuery.d.ts

declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  在jQuery内部,我们还可以使用const、class、enum等语句进行声明:

1
2
3
4
5
6
7
8
9
10
11
12
// src/jQuery.d.ts

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
// src/jQuery.d.ts

declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}

//src/main.ts
jQuery.ajax('/api/get');
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});

泛型

  泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性;简单来说,就是一种创建可复用代码组件的工具,这种组件不止能被一种类型使用,而是能够被多种类型进行复用。

简单的泛型例子

  我们来实现一个重复元素功能的函数,将给定的元素重复给定的次数,最后返回一个数组:

1
2
3
4
5
6
7
8
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呢?

1
2
3
4
5
6
7
function repeatArray(value: any, length: number): Array<any> {
let list = [];
for (let i = 0; i < length; i++) {
list.push(value);
}
return list;
}

  使用any会导致这个函数可以接收任意类型的参数,这样就导致这个函数缺乏了有效的信息提示,不能告诉函数的调用者传入类型和返回数组中的类型应该是相同的;假设我们传入一个数字,我们只能知道任何类型的值都有可能被返回。

  这样我们就需要通过泛型来定义这个函数:

1
2
3
4
5
6
7
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等其他字母来替代。

  定义泛型函数后,我们可以用两种方式来调用,第一种,传入所有的参数,包含类型参数:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
let list1 = repeatArray<string>("5", 3);
let list2 = repeatArray<number>(6, 5);

  第二种方式,利用类型推论,让编译器自动确定类型变量的类型:

1
2
let list1 = repeatArray("5", 3);
let list2 = repeatArray(6, 5);

多个类型参数

  在定义函数时,我们有可能会用到多个泛型变量,,用逗号分隔这多个变量:

1
2
3
4
5
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  在swap函数中,我们通过2个变量来交换输入的数组中的元素

泛型类

  我们不仅用泛型定义函数,还可以用泛型定义一个类,和函数类似,也是通过<T>跟在类名后面:

1
2
3
4
5
6
7
8
9
10
11
12
class Animal<T> {
name: T;
constructor(name: T) {
this.name = name;
}
sayName(): T {
return this.name;
}
}

let dog = new Animal("tom");
dog.sayName()

泛型约束

  在函数内部,如果需要使用泛型变量上的属性,由于不知道它的类型(等同于Unknow类型),因此不能随意调用属性和方法:

1
2
3
4
5
6
function showLength<T>(arg: T): T {
//报错:
//Property 'length' does not exist on type 'T'.
console.log(arg.length);
return arg;
}

  泛型变量T不一定有属性length,因此会报错;我们可以对泛型变量进行约束,只允许传入包含length属性的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 })

// 报错:
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'
showLength(5)

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  另外,多个泛型参数之间也可以互相约束:

1
2
3
4
5
6
7
8
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基础篇中,我们通过接口来定义了函数表达式:

1
2
3
4
5
6
interface ISumFunc {
(x: number, y: number): number;
}
let sum: ISumFunc = function (x, y) {
return x + y;
};

  同时可以使用含有泛型的接口来约束函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).

/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  也可以通过files显式指定需要编译的文件:

1
2
3
4
5
{
"files": [
"./some/file.ts"
]
}

  还可以使用includeexclude选项来指定需要包含的文件和排除的文件:

1
2
3
4
5
6
7
8
9
10
{
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules"]
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  includeexclude支持的glob通配符有:

  • *匹配0或多个字符(不包括目录分隔符)
  • ?匹配一个任意字符(不包括目录分隔符)
  • **/递归匹配任意子目录

在ts中使用ESLint和Prettier

  有些童鞋可能会有疑惑了,ts在编译阶段就能排查出代码错误,为什么还需要用到eslint来检查呢?因为ts重点关注的是类型的检查,而不是代码和风格的检查,有一些代码的问题,比如==与===的检查、禁用var等功能,还是需要eslint来配合;首先在项目中安装eslint的依赖:

1
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中配置插件:

1
2
3
4
5
6
7
8
9
module.exports = {
parser: '@typescript-eslint/parser', //定义ESLint的解析器
extends: ['plugin:@typescript-eslint/recommended'],//定义文件继承的子规范
plugins: ['@typescript-eslint'],//定义了该eslint文件所依赖的插件
env:{ //指定代码的运行环境
browser: true,
node: true,
}
}

  在一文彻底读懂ESLint中还介绍了Eslint配合了Prettier,在ts项目,我们也可以搭配Prettier来格式化代码,首先也是进行安装:

1
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装饰器接口一个对象做参数,可以在对象中声明componentsfiltersdirectives等装饰器的选项,也可以声明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功能相同,接收一个参数,这个参数可以有三种写法:

1
2
3
4
5
6
7
8
9
<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方式传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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,并将其作为事件名。

1
2
3
4
5
6
7
8
9
10
<script lang="ts">
export default class Home extends Vue {
@Emit()
clickBtn() {
}
@Emit('click-my-btn')
clickBtn1() {
}
}
</script>

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  最后相当于以下代码:

1
2
3
4
5
6
7
8
9
10
<script>
export default {
clickBtn(){
this.$emit('click-btn')
}
clickBtn1(){
this.$emit('click-my-btn')
}
}
</script>

  @Emit会将回调函数的返回值作为第二个参数返回给父级函数,如果没有返回值,则会默认使用括号里的参数:

1
2
3
4
5
6
7
8
9
10
11
<script lang="ts">
export default class Home extends Vue {
@Emit()
returnVal() {
return 'hello parent'
}
@Emit()
clickBtn(ev) {
}
}
</script>

  等同于以下代码:

1
2
3
4
5
6
7
8
9
10
<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>

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里


本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。