写ts也有不短的时间了,先凌乱的整理一下日常容易让人困惑的点吧,后续可能按条理整理一下。
type和interface
在主要使用场景下,type和interface的功能是比较重合的,这也是容易造成人困惑的地方。
都能做
描述对象结构
1 | interface User { |
描述函数
1 | type T_Callback<T> = (data: T) => void; |
通过implements实现接口
type和interface都可以用implements关键字来被class实现1
2
3
4
5
6
7//type A<T> = {age: number,fn: (data: T) => void; }
interface A<T> {age: number,fn: (data: T) => void; }
class CLASS_A implements A<number> {
age: 1
fn: (data: number) =>{}
}
let a:CLASS_A = new CLASS_A()
type可以而interface不行
别名
可以用type定义其它interface/type的别名1
2
3type NewNameOfA = A
type Mystr = string
type S_A_MAP = Map<string, A>;
联合类型
1 | type ALL = A | B |
interface可以而type不行
重复定义自动扩展
1 | interface A {name: string} |
会自动扩展属性
用extends关键字进行扩展
interface能用extends关键字进行扩展,type只能用&
1
2
3
4
5
6type T_A = {}
type T_B = {} & T_A //type只能用 &
interface A{}
interface B extends A{} //interface扩展使用extend
interface C extends T_A{} //interface用extend扩展type
type T_C = {} & A //type用&扩展interface
最佳实践
能用interface就用interface,只能用type的场景再用type。
官网的说法是遵守扩展开放修改封闭的原则,使用interface更佳。这里可能是interface对extends
的支持?体会不是很深,后续如果有发现具体场景再补充。
参考 https://www.tslang.cn/docs/handbook/advanced-types.html 中的 接口 vs. 类型别名
any、unknown和never
any在声明和调用时都放弃了类型检查,相当于退化成了js。下面的代码会带来运行时异常:1
2
3
4
5let user:any = {
name: "abc",
hi: ()=>{console.log("hi")}
}
user.hello()
unknown在声明时放弃类型检查,但是调用时必须先明确类型,不会带来运行时异常。
通过类型推断明确类型:1
2
3
4
5
6
7
8
9
10interface HI {
hi: ()=>void
}
let user: unknown = {
a: 1,
hi:()=>{console.log("hi")}
}
//user.hi() //编译阶段就会异常
let hiUser = user as HI //通过类型断言明确类型
hiUser.hi()
通过类型判断明确:1
2
3
4
5
6
7
8
9
10function handleUnknown(data: unknown){
if(typeof data == "string"){
return data.toLowerCase()
}
if(typeof data == "number"){
return data.toFixed(2)
}
}
console.log(handleUnknown("STR"))
console.log(handleUnknown("123.456"))
这样无论输入啥类型都不会翻车
而never
表示永远不会有的类型,一般用于直接抛出异常的函数,或者含有死循环的函数等场景。1
2
3
4function test1():never{
throw new Error()
}
let a = test1()
函数声明
函数声明相关的东西老是忘,这里特别记录一下吧。 ts声明函数类型时,分为普通函数和构造函数。
普通函数需要关注的就两个东西:参数类型、返回值类型。这里要特别注意的是,只关注类型而不关注参数名。接口和type都可以声明函数类型。
构造函数类型复制得用class,而不能用fn,这个也可以参考下面“类”中的理解。
interface声明函数类型1
2
3
4interface FN {
(x: number, y: number): number
}
let add1: FN = (a: number, b: number) => { return a+b }
type声明函数类型1
2type FN_T = (x: number, y: number) => number
let add2: FN_T = (a: number, b: number) => { return a+b }
直接写出函数类型1
let add3:(x: number, y: number) => number = (a: number, b: number) => { return a+b }
构造函数类型1
2
3
4
5
6
7interface CONSTRUCTOR_FN {
new (x: number, y: number)
}
let add4:CONSTRUCTOR_FN = class A {
constructor(a:number, b:number){}
}
console.log(new add4(1,3))
与众不同的”类”
构造类型与实例类型
ts的类实际上由两个部分组成:构造函数类型和实例类型。
当我们定义一个类:1
2
3class A{
age: number;
}
这里名字A在不同的使用场景下有着不同的含义
- 当
A
作为变量名时,它代表着类A
这个对象 - 当
A
作为参数类型或者返回值类型时,它代表着类A的实例类型
- 当用
new A()
来实例化一个类A的对象时,它代表着类A的构造函数
,它的类型是typeof A
1
2
3
4
5
6
7
8
9
10
11
12
13
14class A{
age: number
}
// A作为变量,此时A代表 类A 这个对象
let B = A
//A用来表示参数类型或者返回值类型,代表 类A 的实例类型
function test(data?: A): A{
//用new A来初始化对象,代表类Ade构造函数 constructor A(): A
let a = new A()
a.age = 2
return a
}
let a = test()
console.log(a.age)
ts类的js编译结果分析
ts最终运行时还是需要编译为js的,所以我们需要理解一下ts的class究竟是如何实现的,其编译后长的是啥样。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Test{
public uname: string
private _age: number
constructor(name:string, age: number){
this.uname = name
this._age = age
}
public echoName(){
return this.uname
}
private _getAge(){
return this._age
}
}
用tsc
编译后,得到一个js的代码1
2
3
4
5
6
7
8
9
10
11
12
13var Test = /** @class */ (function () {
function Test(name, age) {
this.uname = name;
this._age = age;
}
Test.prototype.echoName = function () {
return this.uname;
};
Test.prototype._getAge = function () {
return this._age;
};
return Test;
}());
可以比较明显的得到几个结论:
- 类对象本身实际上就是一个构造函数
- 成员属性用构造函数内部属性表示,类实例之间独立
- 成员函数用原型对象中的函数来表示,类实例之间共享
- 运行时实际上丢掉了访问标识符,访问标识符仅在ts静态检查和编译阶段有效
基于上面几个结论,可以想到一些突破ts边界的场景,如1
2
3
4
5let test = new Test("tom", 28)
console.log(test.uname)
//console.log(test._age) //静态检查、编译都会报错
let toAny:any = test
console.log(toAny._age) //可以拿到私有变量的值
强行访问私有变量1
2
3
4
5
6
7
8let cat = new Test("cat", 18)
let dog = new Test("dog", 19)
console.log(cat.echoName(), dog.echoName())
let catAny:any = cat
catAny.__proto__.echoName = ()=>"pig"
console.log(cat.echoName(), dog.echoName())
//cat dog
//pig pig
通过一个对象改掉某个类所有实例的成员函数
作为参数传递时的类型表示方法
类对象作为参数传递时,用类名约束其类型就可以了。
类作为构造器传入时,则需要用 typeof 类名
,获取其构造函数类型。1
2
3
4
5
6
7
8
9
10
11
12class A{
age: number
}
function test1(a:A):A{
return a
}
function test2(classA: typeof A): A{
return new classA()
}
let a1 = test1(new A())
let a2 = test2(A)
console.log(a1, a2)
上面的示例中,classA的类型提示是 new () => A
,也就是说,test2函数可以接受任意一个返回A对象的构造函数。
ts中所有类的构造函数类型都可以表示为1
interface Handle { new (...args: any): any}
例如我们想写一个泛型函数,接受各种类型的构造器,返回它们的实例类型,那可以这样写1
2
3
4
5
6
7
8interface Handle { new (...args: any): any}
function createA<T extends Handle>(type: T):InstanceType<T>{
return new type()
}
class A{
age: number
}
let a = createA<typeof A>(A)
这里注意两个地方,第一个是泛型T的约束是T extends Handle
,表示T必须是一个构造函数类型;
第二个是,因为T是构造函数类型,所以返回的实例类型就必须是InstanceType<T>
,如果返回值类型写成T
的话,表示的实例类型就不是类型的实例对象,而是类型的构造函数对象。
内置类型操作、类型推断
ts内置了一堆类型操作符,包括类型访问属性、类型内容修改、类型推断等。
ts中extends与infer一起使用可以用来做类型推断,上面提到的InstanceType<T>
其实就是一种类型推断,推断出构造函数T所返回的实例类型。
ts内置了一些类型推断表达式,总量也不多,可以列举出来。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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111/**
* 将类型T的所有属性设置为可选,作为一个新的类型
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
/**
*将类型T的所有属性设置为必选,作为一个新的类型
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
/**
* 将类型T的所有属性设置只读,作为一个新的类型
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
/**
* 从T中选择一部分属性,这些属性类型K必须是T的属性的一部分,作为一个新的类型
* 用于从T中挑选出一部分在某集合的属性
*/
// interface A{
// name: string
// age: number,
// height: number
// }
// type B = Pick<A, "age" | "height">
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
/**
* Construct a type with the properties of T except for those in type K.
* 构造一个具有T的属性的类型,不包含K类型中的属性。
* 用于从T中挑选出一部分不在某集合的属性,与Pick操作相反
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
/**
* 基于类型A,构造一个任意类型的 key→T的字典类型
* 用于快速构造字典类型
* type C = Record<string | 9, A>
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
/**
* Exclude from T those types that are assignable to U
* 如果U包含T,则表示T类型,否则表示never类型
* 用于构造非派生自U的类型
* type D1 = Exclude<A, B> //never
* type D2 = Exclude<B, A> //B
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
* 与Exclude想法,判断继承关系,T一定要是继承自U的
* 用于构造派生自U的类型
*/
type Extract<T, U> = T extends U ? T : never;
/**
* 构造非null和undefined的类型
*/
type NonNullable<T> = T extends null | undefined ? never : T;
/**
* 通过infer推断获取函数的参数类型
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
/**
* 通过infer推断获取构造函数的参数类型
*/
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
/**
* 通过infer推断获取函数的返回类型
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
/**
* 通过infer推断获取构造函数的返回类型,即类的实例类型
*/
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
/**
* 转大写
*/
type Uppercase<S extends string> = intrinsic;
/**
* 转小写
*/
type Lowercase<S extends string> = intrinsic;
/**
* 首字母大写
*/
type Capitalize<S extends string> = intrinsic;
/**
* 首字母小写
*/
type Uncapitalize<S extends string> = intrinsic;
一个用infer获取元组中数据类型的,很有意思例子:1
2
3type ElementOf<T> = T extends Array<infer E> ? E : never;
type TTuple = [string, number];
type ToUnion = ElementOf<TTuple>; // string | number
黑科技写法:1
2type TTuple = [string, number];
type Res = TTuple[number]; // string | number
装饰器与元数据
装饰器
首先来理解一下装饰器是什么。简单来说,装饰器是一个在代码运行时应用在类定义过程上函数。可以从这里个关键点来理解:
- 是什么:装饰器是一个有格式要求的函数
- 执行时机:在定义类的时候执行
- 干什么:用来修饰类本身、类的成员变量、方法、方法参数
基于上面的理解,装饰器只能作用于ts类定义阶段,不能作用于函数或者其它变量上。ts定义的装饰器函数有这么几种:1
2
3
4declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
类装饰器
1 | declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; |
类装饰器是长这样的一个函数,它接收的唯一一个参数是类的构造函数。可以看到,它的返回值是TFunction或者void。在返回void时,这个装饰器的作用可能是做在类的构造函数上定义一些元数据等操作;返回TFunction时,则会用返回的新构造函数替换掉老的,也就是说被装饰的类此时已经变成另一个类了。1
2
3const normalClassDecorator:ClassDecorator = (target)=>{
console.log("normalClassDecorator:", target.toString())
}
定义一个普通的类装饰器,用来做给类加元数据等操作1
2
3
4
5
6
7
8
9
10const replaceClassDecorator = <T extends new (...args)=>any>(target:T)=>{
console.log("replaceClassDecorator:", target.toString())
return class extends target{
newProp = "xx"
constructor(...args){
super(...args)
this.age = 99
}
}
}
定义一个替换类的类装饰器。这里有两点要注意的是:
- 如果返回的类不继承被装饰的类,那么返回类和被装饰类的构造函数必须一致
- 如果返回的类继承被装饰的类,那么要么返回类不重写构造函数,如果要重写,则必须有个仅接收
...args:any[]
作为参数的构造函数
1 | @normalClassDecorator |
多个类装饰器同时使用时,离类名近的先执行,上面的例子就是replaceClassDecorator
先执行。
方法装饰器
1 | declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; |
方法装饰器接收的三个参数分别是:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 成员的名字。
- 成员的属性描述符。
如果方法装饰器返回一个值,它会被用作方法的属性描述符,这时候就可以做一些例如修改descriptor中的value来劫持方法等骚操作。
这里注意,方法装饰器同样可以试用在属性的 getter
或setter
上,不过要注意这俩只能装饰一个,对第一个生效。
属性装饰器
1 | declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; |
属性装饰器接收的参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字
由于属性装饰器没办法在类的对象实例化之前访问到真正的属性值,所以它能做的事情比较有限,一般用于根据成员名字创建一些元数据啥的。
一个用属性装饰器给属性定义元数据的使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
console.log(new Greeter(" tencent").greet())
参数装饰器
1 | declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void; |
参数装饰器接收的三个参数分别是:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 成员的名字
- 参数在函数参数列表中的索引
一个用参数装饰器+方法装饰器合作,来做参数校验的示例: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
41const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
//获取已经存在的必选参数索引数组,没有则取值为空数组
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
//将当前index加入到数组里边去
existingRequiredParameters.push(parameterIndex);
//将不能重复的参数index设置回元数据
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
//缓存原始方法
let method = descriptor.value;
//将方法体替换一下,加上参数校验
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
//检查参数是否缺失
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name?: string) {
return "Hello " + name + ", " + this.greeting;
}
}
console.log(new Greeter("tencent").greet("wuhan"))
console.log(new Greeter("tencent").greet()) //抛出运行时异常
应用顺序
多种装饰器共存时,调用顺序会按实例成员、静态成员、构造函数、类的顺序来:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
多个相同类型的装饰器执行时,先执行写的离被装饰对象近的。
装饰器工厂
注意到一些文档单独把这个概念拿出来说,其实可以用一句话描述:返回一个装饰器函数的函数。
实际使用时也经常把一个装饰器工厂的调用
当做装饰器来用,例如nestjs中的 Get("/xxx/path")
元数据
Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,支持的方式是通过一个reflect-metadata
模块来引入Reflect Metadata特性。
这里列举一下reflect-metadata
中提供的api用法吧。
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | //目标对象或其原型链是否定义了提供的元数据key |
1 | /** |
1 | /** |
1 | /** |
模块和命名空间
这里知识比较零碎,慢慢补全吧
命名空间
命名空间合并(和命名空间、和类等)
和命名空间合并1
2
3
4
5
6
7
8namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
和类合并,作用是为类扩展静态属性或者内部类等,注意namespace内的东西必须export出来1
2
3
4
5
6
7
8class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { } //为类扩展了个内部类
export PREFIX:string = "xxx" //为类扩展了个静态属性
}
和函数合并,与类合并一样,也是扩展了一些静态属性1
2
3
4
5
6
7
8
9
10function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
console.log(buildLabel("Sam Smith"));
举一反三,枚举类型或者其它别的类型, 都可以用namespace来扩展静态属性。
在声明文件中导出用顶级对象表示的API
如@tars/utils的声明文件中的一部分1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21export namespace timeProvider {
/** 采用 `Date.now()` 的方式获取时间, 此种方式效率最高 */
export function nowTimestamp (): number
/**
* 当前时间相对于 `oTime` 的时间间隔, 与 `nowTimestamp` 配对使用
* @param oTime 相对时间, 由 `nowTimestamp` 函数返回
* @returns 浮点类型, 时间间隔, 单位毫秒
*/
export function diff (oTime: number): number
/** 获取当前的时间戳, 即机器从启动到当前的时间 `process.hrtime` */
export function dateTimestamp (): Timestamp
/**
* 当前时间相对于 `oTime` 的时间间隔, 与 `dateTimestamp` 配对使用
* @param oTime 相对时间, 由 `dateTimestamp` 函数返回
* @returns 浮点类型, 时间间隔, 单位毫秒
*/
export function dateTimestampDiff (oTime: Timestamp): number
}
本文链接:https://www.zoucz.com/blog/2021/05/29/a2079490-c029-11eb-9fe7-534bbf9f369d/