# typescript 进阶
# 泛型
泛型可以说一个泛指类型,在程序时并不清楚它具体是哪一个类型,这段程序被调用时才会给它指定具体类型。泛型定义时常使用<T>
,当然使用<A>
也可以;在泛型调用就得给它指定具体的类型比如<string>
。
# 泛型接口
泛型接口中<T>
常跟在接口名后面,一般调用处要指定具体类型。泛型可以一次使用多个,不止是泛型接口,下面的比如泛型类泛型函数等都可以使用多个泛型。
interface ITask<T, P> {
name: T;
age: P;
}
const task: ITask<string, number> = { name: "Bob", age: 20 };
2
3
4
5
# 泛型类
泛型类中<T>
常跟在类名后面,一般调用处要指定具体类型。
class Data<T> {
constructor(private _item: T) {}
get item(): T {
return this._item;
}
}
const data: Data<number> = new Data<number>(0);
2
3
4
5
6
7
# 泛型函数
泛型函数中<T>
常跟在函数名后面,一般调用处要指定具体类型。含有可以给一个变量类型注解为泛型函数,一般有两种,一种是花括号和冒号搭配,另一种是不用花括号而使用箭头函数。
function fun<T>(v1: T, v2: T) {
console.log(`${v1}+${v2}`);
}
fun<string>("web", "app"); // 调用时加上<string>具体的限制类型
const func: { <T>(v1: T, v2: T): void } = fun; // 定义一个泛型函数
const func1: <T>(v1: T, v2: T) => void = fun; // 定义一个泛型函数
2
3
4
5
6
# 泛型约束
泛型还可以和extends
搭配使用,表示该泛型受到约束
interface Itest {
name: string;
}
class Data<T extends Itest> {
constructor(private _item: T) {}
get item(): T {
return this._item;
}
}
const data = new Data({ name: "Bob" });
2
3
4
5
6
7
8
9
10
继承接口、类、基本类型都可以
// 使用基本类型的联合类型来约束泛型
class Data<T extends number | string> {
constructor(private _item: T) {}
get item(): T {
return this._item;
}
}
const data: Data<number | string> = new Data("Bob");
2
3
4
5
6
7
8
# 泛型中的 keyof
在程序中使用一个泛型,一般都是直接用它本身,但难免不会去操作它的属性,如果它属性的类型各不相同,在外部调用这段程序时 ts 可能就不会做具体的类型推断了。使用keyof
可以分解一个复杂类型,获取它属性上的各种类型。
interface Task {
num: number;
name: string;
desc: string;
}
class AcceptTask {
constructor(private _task: Task) {}
// 需要根据key去取_task的属性里,key对应的值,但是_task的属性的类型都不一样
// 所以可以使用keyof对Task进行分解,key就可以拿到属性的各种可能的情况
public notify<T extends keyof Task>(key: T): Task[T] {
return this._task[key];
}
}
const status = new AcceptTask({ num: 1001, name: "task1", desc: "xxx" });
status.notify("name");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 命名空间
# 命名空间声明
命名空间(对应在 es6 里叫“内部模块”)可以将一些类或者函数等包裹起来,使用模块化的方式来组织代码,减少类或函数等在全局环境中暴露的机会。
命名空间使用namespace
来声明的,并且还可以和export
搭配使用,可以让命名空间内的一部分内容让外部使用。
view.ts
namespace ViewSpace {
class A {}
class B {}
export class C {
// 将C暴露给命名空间之外,外部就可以使用TaskSpace.C了
constructor() {
new A();
new B();
}
}
}
2
3
4
5
6
7
8
9
10
11
# 命名空间使用
在一个 namespace 中调用另一个 namespace,并且还要放在页面中,那么先使用tsc
进行编译生成对应的 js 文件,然后每一个 js 文件都要用一个<script>
存放,最后就可以在<script>
中使用了。
box.ts
///<reference path='./view.ts'>
namespace BoxSpace {
export class D {
constructor() {
new ViewSpace.C();
}
}
}
2
3
4
5
6
7
8
h.html
<html>
<head>
<script src="./dist/view.js"></script>
<script src="./dist/box.js"></script>
</head>
<body>
<script>
new BoxSpace.D();
</script>
</body>
<html></html>
</html>
2
3
4
5
6
7
8
9
10
11
12
其实可以将编译文件生成在一起,先不考虑使用 webpack,ts 可以自己配置编译生成一个 js。打开 tsconfig.json,将 module 修改为amd
,然后加上"outFile": "./dist/bundle.js"
。这样就只引用 bundle.js 就可以了。
注意:像以上命名空间相互引用的写法,最好要加上“声明”,例如///<reference path='./view.ts'>
# 命名空间里再定义命名空间
///<reference path='./view.ts'>
namespace BoxSpace {
export namespace ButtonSpace {
// 命名空间里再定义命名空间
export class E {}
}
export class D {
constructor() {
new ViewSpace.C();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# import
# 配合 require.js 使用
其实命名空间使用的不多,主要还是使用import方式。跟上一节样,先不考虑使用 webpack,还是使用"module": "amd"
和"outFile": "./dist/bundle.js"
,但在页面使用时会有问题define
识别不了,得引 require.js 进来(用 cdn 形式)。然后在脚本中使用 require 的方式实例化 D,代码示例如下:
view.ts
class A {}
class B {}
export class C {
constructor() {
new A();
new B();
console.log("C");
}
}
2
3
4
5
6
7
8
9
box.ts
import { C } from "./view";
export default class D {
constructor() {
new C();
console.log("D");
}
}
2
3
4
5
6
7
h.html
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js"></script>
<script src="./dist/bundle.js"></script>
</head>
<body>
<script>
require(["box"], function (box) {
new box.default();
});
</script>
</body>
<html></html>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用 webpack 打包
上一节使用的是"module": "amd"
和"outFile": "./dist/bundle.js"
再配合 require.js 来使用的,可以看到还要额外引入 require.js 会比较麻烦。
可以使用 webpack 将 ts 项目编译打包,可以查看webpack的配置和一些用法。比如要将上面的改为 webpack 打包,配置会非常的麻烦,如下:
首先安装 webpack 相关的东西:
npm install --save-dev webpack@5.1.0
,npm install --save-dev webpack-cli@3.3.12
,npm install --save-dev typescript ts-loader
,npm install --save-dev webpack-merge
,npm install --save-dev html-webpack-plugin
,npm install --save-dev clean-webpack-plugin
,npm install --save-dev html-webpack-plugin
,npm install --save-dev uglifyjs-webpack-plugin
。上面的 view.ts 无需调整,box.ts 需要在尾部加一个
new D();
,h.html 要改名为 index.html 并且里面的 script 脚本全部删除。配置项使用生产环境与开发环境分离的模式,一下分别是 webpack.common.js、webpack.dev.js、webpack.prod.js、package.json(部分)。
const path = require("path"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/box.ts", // 入口 output: { filename: "bundle.js", // 出口的文件名 path: path.resolve(__dirname, "dist"), // 处理输出位置 }, module: { rules: [ { // 使用tsloader test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/, }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], }, plugins: [ new CleanWebpackPlugin({ // 打包生成dist前会自动删除dist下的文件,使用npm脚本“rm -rf ./dist”也可以 cleanOnceBeforeBuildPatterns: ["dist"], }), new HtmlWebpackPlugin({ // 用于生成入口html(存于dist),然后会自动引入打包好的bundle.js title: "Production", template: "./index.html", // 模板,webpack生成html时用到的模板,比如模板里要挂载vue。 }), ], };
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
35const path = require("path"); const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); module.exports = merge(common, { devtool: "inline-source-map", // 开发环境所使用的SourceMap devServer: { // webpack-dev-server contentBase: path.resolve(__dirname, "dist"), // 设置服务器访问的基本目录 host: "localhost", // 服务器的ip地址 port: 8080, // 端口号 open: true, // webpack运行服务器时自动打开页面 }, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14const merge = require("webpack-merge"); const UglifyJSPlugin = require("uglifyjs-webpack-plugin"); const common = require("./webpack.common.js"); module.exports = merge(common, { devtool: "source-map", // 用于生产的SourceMap plugins: [ // 代码混淆压缩,当然你也可以考虑使用BabelMinifyWebpackPlugin或ClosureCompilerPlugin new UglifyJSPlugin({ sourceMap: true, // 用于生产的SourceMap }), // 为项目设置环境变量,指定环境为生产环境,打包出的bundle会小很多。 // 也可以在package.json里加上--env.NODE_ENV=production,但是得把env传到webpack.prod.js里来,将对象改为函数 new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production"), }), ], });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18"scripts": { "serve": "webpack-dev-server --open --config webpack.dev.js --mode development", "build:dev": "webpack --open --config webpack.dev.js --mode development", "build:pro": "webpack --config webpack.prod.js --mode production" },
1
2
3
4
5
# 编写*.d.ts 文件
在 ts 项目中引入一个 js 库,如果这个 js 库没有对应的ts 声明文件,那在使用时就会报错,但是运行时是正确(前提是你真的写对了),也就说静态类型检测不通过但实际上运行没问题。在编写ts 声明文件时,要对这个 js 库非常熟悉才能知道要声明成函数或者变量或者对象。
# 声明全局函数
jQuery.js 这个库在前端经常使用,最经典的就是$
,它经常用于接受一个字符串返回一个对象,返回的对象包含各种方法。声明文件*.d.ts常用 declare 来声明 js 中提供的方法、变量等。
index.html
<html>
<head>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
</head>
<body></body>
<html></html>
</html>
2
3
4
5
6
7
8
done.ts
$(function () {
$("body").html("<div>666</div>");
});
2
3
jquery.d.ts
// 声明一个全局函数$(),其入参也是一个函数
declare function $(param: () => void): void;
// 声明一个全局函数$(),入参是一个字符串,返回一个对象,这个对象有个html属性(方法)
declare function $(
selector: string
): {
html: (param: string) => void;
};
2
3
4
5
6
7
8
# 搭配使用 interface
使用 jquery 中的一些方法,基本都要返回一个页面元素,这个页面元素其实是个对象,比如html()
,可以使用 interface 来约束这个元素对象。还可以对$
本身使用 interface,并用 interface 约束它的多种传值方式。
// jquery对象(页面元素)
interface IjqueryDom {
html: (param: string) => IjqueryDom;
}
// $的几种用法,使用interface来约束
interface Ijquery {
(param: () => void): void; // 第一种传值方式:递函数(立即运行)
(selector: string): IjqueryDom; // 第二种传值方式:传递字符串用于获取页面元素
}
// 声明一个全局变量$
declare var $: Ijquery;
2
3
4
5
6
7
8
9
10
11
# 声明对象和类
在使用 jquery 可能会遇到$.fn.init();
这种对象的对象的方法,可以搭配 namespace 来对对象进行声明,namespace 中还能嵌套 namespace
done.ts
$(function () {
$("body").html("<div>666</div>");
$.fn.init();
});
2
3
4
jquery.d.ts
// 声明一个全局函数$(),其入参也是一个函数
declare function $(param: () => void): void;
// 声明一个全局函数$(),入参是一个字符串,返回一个对象,这个对象有个html属性(方法)
declare function $(selector: string): IjqueryDom;
// 声明一个对象,有个属性fn也是对象,fn有个属性是个方法
declare namespace $ {
namespace fn {
function init(): IjqueryDom;
}
}
// jquery对象,有个html属性(方法)
interface IjqueryDom {
html: (param: string) => IjqueryDom;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# es6 模块的声明文件
去掉 html 中的<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
,然后局部安装npm install jquery --save
。
es6 模块方式使用 jquery,那就是import $ from 'jquery';
,但是这个jquery
会报错“无法找到模块“jquery”的声明文件”,那么我们就需要对jquery
这个 es6 模块进行声明文件的编写。
done.ts
import $ from "jquery";
$(function () {
$("body").html("<div>666</div>");
$.fn.init();
});
2
3
4
5
jquery.d.ts
// 声明一个es6的模块
declare module "jquery" {
// jquery对象
interface IjqueryDom {
html: (param: string) => IjqueryDom;
}
// 这个模块中$是个方法
function $(param: () => void): void;
// 这个模块中$是个方法,重载
function $(selector: string): IjqueryDom;
// 这个模块中$还是个对象
namespace $ {
namespace fn {
function init(): IjqueryDom;
}
}
// 最后一定记得导出,而且都是export = xxx的形式
export = $;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 装饰器
学习装饰器,必须清楚 es5 的构造函数与 es6 的类的联系。
装饰器(Decorators)可以使用元编程的语法对类以及成员来进行监视、修改或替换。其实就是在类定义的时候,将类传到事先准备好的函数里,经过函数的运作,类就获得或者按照要求修改了一些东西。编译时动态修改了程序,这种方式叫做元编程。
装饰器在 ts 中是一项实验性特性,要使用它得先打开它的相关配置。打开 tsconfig.json 文件,加上"experimentalDecorators": true
和"emitDecoratorMetadata": true
。
装饰器的使用这样的,@expression
,其中 expression 最后的求值结果必须是一个函数。如果对类进行了多个装饰器修饰,那么首先依次从上到下对装饰器表达式求值,然后求值的结果会被当作函数并从下到上依次调用。
function f() {
console.log(1);
return function <T>(constructor: T) {
console.log(2);
};
}
function g() {
console.log(3);
return function <T>(constructor: T) {
console.log(4);
};
}
@f()
@g()
class C {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面就是装饰器的例子,打印顺序是1 3 4 2
,f()和 g()返回的函数会为 C 这个类作修饰。
# 对类装饰
对类使用装饰器,可以用来监视或修改类定义。在装饰器表达式里,类以构造函数形式作为其唯一的参数。装饰器表达式不返回值时,一般就是简单地往原型上添加内容;而如果是返回一个值,该值就是个匿名类并且会继承以前的类,也就是说匿名类会覆盖原类的同名属性和方法(包括 constructor 方法)。
在 ts 中构造函数形式可以是new(...args: any[]) => {}
,也可以是{ new(...args: any[]): {} }
,当然最简单的是Function
。具体在装饰器中使用构造函数形时,传入的具体类型各不相同,那么有必要使用泛型,也就是T extends new(...args: any[]) => {}
。
// constructor是个构造函数(其实就是Greeter),但不清楚具体类型得用泛型(不然ts静态检查过不了)
function classDecorator<T extends new (...args: any[]) => {}>(constructor: T) {
// 返回这个class会以继承的方式覆盖原类的方法和属性(主要影响同名的,不同名的就相当于新加内容)
return class extends constructor {
public newProperty = "new property";
public hello = "override";
};
}
@classDecorator
class Greeter {
public property = "property";
public hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上面输出的结果{ property: 'property', hello: 'override', newProperty: 'new property' }
,可能你会意外 hello 的值为什么是override
而不是world
,其实你需要补充一下 es6 的类知识了。在继承的派生类里没有明写constructor()
方法时,其实是隐藏调用了。比如上面的代码其实是下面这样一个形式:
// 部分代码,其他一样的省略
return class extends constructor {
public newProperty = "new property";
public hello = "override";
constructor(...args: any[]) {
super(...args);
}
};
2
3
4
5
6
7
8
你再根据这段代码加上前面所说的装饰器修饰类时的规则就能明白 hello 为什么是override
,首先public hello = "override"
覆盖原有的public hello: string
,然后constructor(...args: any[])
覆盖原有的constructor(m: string)
,也就是说new Greeter("world")
传进来的world
是不会生效的。而如果改成下面的都就会生效(自己加上this.hello = args[0];
):
// 部分代码,其他一样的省略
return class extends constructor {
public newProperty = "new property";
public hello = "override";
constructor(...args: any[]) {
super(...args);
this.hello = args[0];
}
};
2
3
4
5
6
7
8
9
# 对方法装饰
如果不需要对整个类进行装饰,可以考虑只对类的方法进行装饰。我们知道类里的方法,其实就是构造函数的原型上的方法(不理解的可以去看 es6 的类),那么对类的方法进行装饰,其实就是对原型对象的属性进行操作,是不是立马想到了Object.defineProperty
,这个方法是操作目标对象的属性以及属性的特性。对类的方法进行装饰也是一样的,请看:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
// 对类的方法设置为不可枚举的
function enumerable(value: boolean) {
// target是原型对象,propertyKey是属性名,descriptor是特性,跟Object.defineProperty类似
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 对访问器装饰
只需要私有属性的访问器进行装饰,写法与方法装饰器类似,只是descriptor
不一样而已。但是要注意不允许同时装饰一个成员的 get 和 set 访问器,因为descriptor
同时有这两个访问器。
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
function configurable(value: boolean) {
// target是原型对象,propertyKey是属性名,descriptor是特性,跟Object.defineProperty类似
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 对属性装饰
我们知道普通方法和访问器都可以在原型对象上进行操作,但是类中的属性是属于实例化对象的(需要搞清楚 es6 的类),那么就不能像装饰方法一样使用原型对象上的属性的descriptor
,而是自定义一个descriptor
修改好后然后返回它,从而达到修改属性特性的目的。
class Test {
@propDescriptor(false)
property: string = "name";
}
function propDescriptor(value: boolean) {
// target是原型对象,propertyKey是属性名。注意,在target上不能操作实例属性
return function (target: any, propertyKey: string): any {
const descriptor: PropertyDescriptor = { writable: value };
return descriptor;
};
}
const test = new Test();
test.property = "test"; // 报错,不能对只读的属性进行修改。这样就达到了目的
2
3
4
5
6
7
8
9
10
11
12
13
# 对参数装饰
对类的构造方法或普通方法里的参数进行装饰,装饰器函数接收三个值,第一个是构造函数(方法是静态的)或者原型(普通方法),第二个是参数所在的方法名、第三个就是参数在参数列表中位置的索引。
class Test {
func(name: string, @paramDescriptor(false) age: number) {}
}
function paramDescriptor(value: boolean) {
// target是构造函数(方法是静态的)或者原型(普通方法),propertyKey是参数所在的方法名,descriptor是参数在参数列表中位置的索引
return function (target: any, method: string, index: number): any {
console.log("index", index);
};
}
const test = new Test();
2
3
4
5
6
7
8
9
10
# 装饰器的小例子
const info: any = undefined;
function catchDescriptor(msg: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const fun = descriptor.value;
// 用新函数包裹旧函数,并且统一做异常捕获处理
descriptor.value = function () {
try {
fun();
} catch (e) {
console.log(msg);
}
};
};
}
class Task {
@catchDescriptor("info.name不存在")
getTaskName() {
return info.name;
}
@catchDescriptor("info.id不存在")
getTaskId() {
return info.id;
}
}
const task = new Task();
task.getTaskId();
task.getTaskName();
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