7

TypeScript从平凡到不凡(进阶篇) - 谢小飞的博客

 2 years ago
source link: https://xieyufei.com/2021/09/20/TypeScript-Advance.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
TypeScript从平凡到不凡(进阶篇) - 谢小飞的博客

TypeScrip_

2021年9月20日 晚上

5.1k 字

71 分钟

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

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

$('#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的声明语句放到单独的文件中,这就是声明文件:

// src/jQuery.d.ts
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 letdeclare const声明是最简单的,用来声明一个全局变量类型;let定义的全局变量允许修改,而const定义的则不允许修改

// 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是一个函数,因此我们也可以通过函数的方式来进行定义:

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

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

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

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

// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}

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

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

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

// 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来进行声明:

// src/jQuery.d.ts

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

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

// 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进行嵌套声明:

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

简单的泛型例子

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

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']); // ['seven', 7]

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

  在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 {
//报错:
//Property 'length' does not exist on type '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 })

// 报错:
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'
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来定制我们的编译选项:

{
"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显式指定需要编译的文件:

{
"files": [
"./some/file.ts"
]
}

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

{
"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的依赖:

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', //定义ESLint的解析器
extends: ['plugin:@typescript-eslint/recommended'],//定义文件继承的子规范
plugins: ['@typescript-eslint'],//定义了该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:

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
  • @Prop

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

@PropSync @Model @Watch @Provide @Inject @ProvideReactive @InjectReactive @Emit

  我们来看下每个装饰器的用法:

@Component

  @Component装饰器接口一个对象做参数,可以在对象中声明componentsfiltersdirectives等装饰器的选项,也可以声明computed,watch等。

<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>

  除了上面介绍的属性,还可以注册钩子函数:

<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功能相同,接收一个参数,这个参数可以有三种写法:

  • Constructor:例如String,Number,Boolean等,指定 prop 的类型;
  • Constructor[]:指定 prop 的可选类型;
  • PropOptions:可以使用以下选项:type,default,required,validator。
<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 装饰器接收两个参数:
    propName: string 表示父组件传递过来的属性名;
    options: Constructor | Constructor[] | PropOptions 与@Prop的第一个参数一致;
  • @PropSync 会生成一个新的计算属性。

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

  @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功能相同,监听依赖的变量值变化而做一系列操作,它接收两个参数:

  • path: string 被侦听的属性名;
  • options?: options可以包含两个属性 :
    immediate?:boolean 侦听开始之后是否立即调用该回调函数;
    deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数;
<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的第一个参数一致。
<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同vue中的$ref,接收一个可选字符串,用来指向元素或子组件的引用信息;如果没有这个参数,则使用装饰器后面的属性名:

<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>

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK