首先,面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构 。
OOP特点 = 封装 + 继承 + 多态 ,OOP达到了软件工程的三个主要目标:重用性 、灵活性 和扩展性
面向对象使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程
人总喜欢把我们已知的,或者未知的事务分类,好人?坏人?人?动物?
因为这样归类,给一个具体对象打上标签之后方便我们记忆,甚至是扩展记忆的连贯性。
虽然说有时候具有很强的主观性,但不可否认,这是我们人类最容易记忆的方式。
所以,当我们的程序越来越复杂的时候,就需要一种可读性,可理解性更强,更适合人类的程序设计方式。那就是面向对象。所以在互联网行业,很多流行的后端语言都是面向对象的,比如java,c#等等,因为涉及的业务复杂,为了在这个纷繁复杂的程序业务世界中,找到最清楚的脉络,那就是像我们人类认知世界一样,把各个内容分类,打上标签。
所以面向对象中所说的类,就是对具有相似属性,相似行为的内容进行代码的归类。无非也就是为了开发者记忆,阅读和扩展。
以类型系统的角度来看:一个类,其实就是创建了一种新的数据结构类型。所以一个类,就是一个类型
在ES6之前,传统的JavaScript程序使用构造函数来创建类,基于原型的效果来实现继承。但是这样就让函数有了二义性,简单来说一个构造函数,既能表示是一个函数,也能表示是一个构造函数(类),虽然之前一直约定俗成的默认只要函数名首字母大写就是构造函数,但是却一直没有语法层面的区分,所以ES6之后加上了class和箭头函数,专门用来区分类和一般函数。
1. 访问修饰符
类的主要结构,无非就是属性、构造函数、方法、存取器、访问修饰符、装饰器 ,在typescript中,我们在需要的部分加上类型标注就行了
属性的类型标识类似于变量,而构造函数、方法、存取器的类型标注其实就是函数 :
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 class Animal { #type : string ; constructor ( public name: string , protected color: string , private _age: number , type : string ) { this .#type = type ; } show ( ) { console .log (this .name , this .color , this ._age ) } } class Cat extends Animal { info ( ) { console .log (this .name ) this .show (); console .log (this .name ) this .show (); } } const a = new Animal ("小白" , "白色" , 3 , 'Dog' );console .log (a.name );a.show (); const c = new Cat ("小猫" , "花色" , 2 , 'Cat' );console .log (c.name );c.info ();
注意1: "strict": true
会默认开启"strictPropertyInitialization":true
,也就是属性必须初始化,如果希望关闭这个特性,单独设置"strictPropertyInitialization":false
即可
public
:在类、类的实例、子类 中都能被访问。
protected
:仅能在类与子类中 被访问。
private
:仅能在类的内部 被访问。
#
:仅能在类的内部 被访问。ES2022新特性
需要注意的有两个地方:
1、private
实际是typescript的访问修饰符,因此在转换为js代码之后,实际上是会消失的
2、私有字段#
实际是ES2022中被正式采纳的标准
3、如果不添加任何访问修饰符,默认public
,我们一般不会为构造函数添加修饰符,保持默认即可。
上面构造函数的写法还是略显麻烦,我们其实可以简写:
1 2 3 4 5 6 7 class Animal { #type : 'cat' | 'dog' ; constructor (public name: string , public color: string , private _age: number , type : 'cat' | 'dog' ) { this .#type = type ; } ...... }
用 #
标记的私有字段,目前还不能以类似于 public
和 private
修饰符的构造函数参数简写形式声明
2. 存取器(访问器)属性
访问器属性的写法几乎和JS一样,大家只需要注意一些小细节就行了
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 class Animal { public name : string ; protected color : string ; private _age : number ; #type : string ; constructor (name: string , color: string , _age: number , type : string ) { this .name = name; this .color = color; this ._age = _age; this .#type = type ; } get age () { return this ._age ; } set age (value: number ) { if (value < 0 || value > 150 ) throw new Error ("年龄不符合规范..." ); this ._age = value; } get type (): string { return this .#type ; } set type (value: "Cat" | "Dog" ) { this .#type = value; } show ( ) { console .log (this .name , this .color , this ._age ); } } class Cat extends Animal { info ( ) { console .log (this .name ); this .show (); console .log (this .name ); this .show (); } } const a = new Animal ("小白" , "白色" , 3 , "Dog" );console .log (a.name );a.show (); a.age = -1 ; a.type = "Cat" ; const c = new Cat ("小猫" , "花色" , 2 , "Cat" );console .log (c.name );c.info ();
访问器属性 如果有set,那么默认get返回具体的类型。
所以本身可选属性这种写法和访问器属性get,set一起写就有逻辑上的冲突。
1 2 3 4 5 6 7 8 9 10 11 class Animal { private _age?: number ; get age () { return this ._age ; } set age (value:number ) { if (value < 0 ) throw new Error ('年龄不能为负数' ) this ._age = value; } }
上面的代码就会直接报错:
1 2 不能将类型“number | undefined”分配给类型“number”。 不能将类型“undefined”分配给类型“number”。ts(2322)
修改方式一:当然就是 去掉private _age: number;
的可选属性 ,因为这本来就是和访问器属性冲突
修改方式二: 删除set属性访问器 ,如果set不是必要的,去掉set,当然也能避免这种逻辑冲突
修改方式三: 在get方法中加入对undefined
的判断
1 2 3 4 get age () { if (this ._age === undefined ) throw new Error ('年龄未知' ) return this ._age ; }
修改方式四:非空断言
1 2 3 get age () { return this ._age !; }
修改方式五:
一般我们都会默认打开tsconfig.json
的配置属性"strict": true
,"strictNullChecks": true
当然也随之开启。既然上面报错不能将类型“undefined”分配给类型“number” ,其实就是因为把undefined
作为了单独类型严格检查,当然不能赋值给number
类型。如果不把undefined
和null
作为单独的类型严格检查,当然也就不会报这种错误了。"strictNullChecks": fasle
即可。不过一般不建议随便修改工程配置项
3. 静态成员
在typescript中,你可以使用static关键字来表示一个成员为静态成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Animal { static kingdom = "Animal" ; static showKingdom (): string { console .log (Animal .kingdom ); console .log (this .kingdom ); return `The kingdom is ${Animal.kingdom} .` ; } ...... } const s = Animal .showKingdom ();console .log (s)
在静态方法中虽然也能使用this,但是静态方法中的this
指的不是类的实例,而是类本身。这意味着在静态方法内部,this
指向的是类的构造函数,而不是类的实例。
其实如果我们把target降低到ES5以下(注意 :降低target,如果没有lib的话,默认支持的语法同样也会降级,#
这种写法肯定就不支持了),观察编译之后的js文件,你就会发现静态成员直接被挂载在函数体上 ,而实例成员挂载在原型上
1 2 3 4 5 6 7 8 9 10 Animal .showKingdom = function ( ) { console .log (Animal .kingdom ); console .log (this .kingdom ); return "The kingdom is " .concat (Animal .kingdom ); }; Animal .prototype .show = function ( ) { console .log (this .name , this .color , this ._age ); };
4. 继承
有面向对象,那么肯定就有继承,和JavaScript一样,使用extends
关键字来实现继承,当然类的继承还是具有单根性的特点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Father { constructor (public name: string ) { } info (): void { console .log ('Father Info' ); } } class Child extends Father { constructor (public name: string , public age: number ) { super (name); } override info (): string { console .log ("Child Info" ); super .info (); return "" ; } }
父类和子类的概念相信大家已经不在陌生了,上面主要用到了几个关键字extends
,super
和override
**super
:**在构造方法中调用使用super()
,而且这种写法只能在构造方法中使用,如果子类有构造方法,那么必须调用super()
,无论构造方法有没有参数,这样才可以把父子关系连接起来。不用担心你会忘记,你不写的话会报错!
在一般方法中调用使用super.xxx()
的形式,表示调用的是父类的方法,特别是在子类重写父类的方法中(子类方法和父类方法同名的情况,其实就是覆盖父类的同名方法),这样可以很明显的区分调用的是父类的方法还是子类的方法。super只能调用父类的方法,不能访问父类的属性
override
: Typescript4.x新增的关键字,这是一个提醒关键字。使用override
修饰的方法就表明该方法就是一个重写的覆盖父类的方法。如果子类中的方法没有在父类中出现过,那么不能使用override
5. this返回类型
this可以用作值,也能用作类型。
比如我们现在要做一个ES6的Set
数据结构的简化版:
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 class SimpleSet { private elements : Map <number , boolean >; constructor ( ) { this .elements = new Map <number , boolean >(); } add (element : number ): SimpleSet { this .elements .set (element, true ); return this ; } has (element : number ): boolean { return this .elements .has (element); } delete (element : number ): boolean { return this .elements .delete (element); } values (): number [] { return Array .from (this .elements .keys ()); } } const mySet = new SimpleSet ();mySet.add (1 ).add (2 ).add (3 ); console .log (mySet.values ()); mySet.delete (2 ); console .log (mySet.values ());
其他都没有什么问题,但是如果我们还有一个子类,子类可能需要处理其他的内容。
1 2 3 4 5 class MutableSet extends SimpleSet { show ( ) { console .log ('MutableSet show' ) } }
乍一看没啥问题,但是我们如果再此调用add方法,就能看到具体的一些区别了
1 2 3 const mySet = new MutableSet ();mySet.add (1 ).add (2 ).add (3 ).show ();
所以,为了保证子类的this返回正确,只能重写覆盖父类的add方法,目的仅仅就只是为了改写返回的对象类型
1 2 3 4 5 6 7 8 9 class MutableSet extends SimpleSet { add (element : number ): MutableSet { super .add (element); return this ; } show ( ) { console .log ('MutableSet show' ) } }
其实,我们可以用一个简单的办法就能解决,在父类中,add方法的返回类型直接定义为this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class SimpleSet { add (element : number ): this { this .elements .set (element, true ); return this ; } ...... } class MutableSet extends SimpleSet { show ( ) { console .log ('MutableSet show' ) } } const mySet = new MutableSet ();mySet.add (1 ).add (2 ).add (3 ).show ();
6. 抽象类
面向对象中还有一个概念:抽象类 。抽象类是对类结构与方法的抽象。
面向对象的三大特征: 封装 继承 多态
关于后端语言对于多态的理解:
多态是同一个行为具有多个不同表现形式或形态的能力
6.1 多态存在的三个必要条件
继承
重写
父类引用指向子类对象:Parent p = new Child();
后端java伪代码实现:
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 abstract class Shape { abstract void draw (); } class Circle extends Shape { void draw ( ){ System .out .println ("Circle" ) } } class Triangle extends Shape { void draw ( ){ System .out .println ("Triangle" ) } } class Square extends Shape { void draw ( ){ System .out .println ("Square" ) } } Shape s = new Square ();s.draw ();
6.2 TS中抽象类的语法
1 2 3 4 5 6 7 8 abstract class Foo { abstract name : string ; abstract get nameGetter (): string ; abstract method (name : string ): string ; static info (): void { console .log ('Foo Info' ); } }
注意,抽象类中的成员也需要使用 abstract
关键字才能被视为抽象类成员,反过来,如果一个类中有abstract
修饰成员,那么必须在抽象类中
如果抽象类中全部是抽象成员,抽象方法或者静态方法,子类可以使用implements
实现,当然也可以使用extends
1 2 3 4 5 6 7 8 9 class Baz implements Foo { name = 'baz' ; get nameGetter (): string { return this .name ; } method (name : string ): string { return name; } }
当然最关键的,抽象类并不能被实例化 ,抽象类出现的目的就是为了程序架构而生的,抽象类中全部都是抽象的方法,所以也没有实例化的必要。
如果抽象方法中,有实例方法,那么子类必须要使用extends
继承
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 abstract class Foo { abstract name : string ; abstract get nameGetter (): string ; abstract method (name : string ): string static info (): void { console .log ('Foo Info' ); } foo ( ) { console .log ('foo' ) } } class Baz extends Foo { name = 'baz' ; get nameGetter (): string { return this .name ; } method (name : string ): string { return name; } } const baz = new Baz ();const b = baz.method ("hello" );console .log (b)baz.foo ();
其实道理很简单,当抽象类中编译之后其实在js(ES6)中还是一个普通的class类,全是抽象的,其实相当于什么都没有,这个实现关系只存在于TS中,所以使用implements
还是extends
都没有关系,对编译之后的js文件都没有影响。
但是如果抽象类中有具体的方法了,编译之后的js文件的class类中是有内容的,所以必须要使用extends
如果一个类中,并没有全部实现抽象类的抽象方法,那么这个类也必须是抽象的。
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 abstract class Foo { abstract name : string ; abstract get nameGetter (): string ; abstract method (name : string ): string ; static info (): void { console .log ('Foo Info' ); } } abstract class Baz extends Foo { method (name : string ): string { return name; } } class bar extends Baz { name = 'bar' ; get nameGetter (): string { return this .name ; } method (name : string ): string { return name; } }
7. 接口
面向对象中,另外一个非常重要的概念当然就是接口,按照面向对象的说法来说,接口是比抽象类还要抽象的概念,在接口中就只能有抽象的方法和属性,相当于就只能声明结构,不能有具体的实现。
比如上面的代码,我们改为interface实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 interface Foo { name : string ; get nameGetter (): string ; method (name : string ): string } interface Bar { show (addr : string ): string } class Baz implements Foo , Bar { name = 'baz' ; get nameGetter (): string { return this .name ; } method (name : string ): string { return name; } show (addr : string ): string { return addr; } }
在面向对象的语法特性中,接口主要是几点点:
1、类和接口的实现使用implements
关键字
2、可以多实现
3、在接口中不能使用访问修饰符
4、接口和接口之间使用extends继承,而且可以多继承
其实通过编译之后,你会发现,抽象类至少还有一个空的类在那里,而interface
的声明在编译之后的js文件夹中就直接删掉了
在我们不考虑面向对象这些特性的时候,纯前端程序员在使用接口的时候,是直接和类型别名type
进行类比的。虽然继承,实现等也是表示类型关系。但是对于初学者来说,当我们需要一种类型的时候,用interface
或者类型别名type
来表示一种类型结构,可以减少很多心智负担。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 interface User { readonly id : number name : string show : (addr: string ) => void } type Person = { readonly id : number name : string show : (addr: string ) => void } const u : User = { id : 1 , name : 'ruby' , show (addr : string ): void { console .log (addr) } } u.id = 2 ;
7.1 声明合并
interface还有一种很重要的特性就是声明合并,就是typescript自动的把多个同名的interface
接口声明组合在一起,当然这个特性不单单是接口有,枚举和namespace命名空间也有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 interface User { readonly id : number name : string show : (addr: string ) => void } interface User { age : number } const u : User = { id : 1 , name : 'ruby' , show (addr : string ): void { console .log (addr) }, age : 13 }
注意:虽然可以合并,但是并不能有冲突,如果出现键同名但是值不同类型的情况会报错
1 2 3 4 5 6 7 8 9 interface User { readonly id : number name : string show : (addr: string ) => void age : string } interface User { age : number }
当然,接口和接口,接口和类型别名之间也能有继承关系,使用extends
关键字。接口要继承类型别名,类型别名代码的类型应该是一个结构类型
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 interface User { id : number name : string } interface Person extends User { age : number } type Action = { type : string get (): string set (v : string ): void } interface Person extends Action { sex : "男" | "女" } const p : Person = { id : 1 , age : 13 , name : 'ruby' , sex : "男" , type : 'person' , get (): string { return 'hello' }, set (v : string ): void { console .log (v) } }
其实反过来,我们用type来实现也行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type PA = Person & Action & { sex : "男" | "女" }const p : PA = { id : 1 , age : 13 , name : 'ruby' , sex : "男" , type : 'person' , get (): string { return 'hello' }, set (v : string ): void { console .log (v) } }
8. 接口与类型别名的区别
这是一道在笔面试中经常出现的TS问题。当然你可以像背八股文一样,如下面这样回答:
在对象结构扩展情况下,interface 使用 extends 关键字,而 type 使用交叉类型(&
)。
同名的 interface 会声明合并,当然需要兼容原接口的结构。
interface 与 type 都可以描述对象类型、函数类型、Class 类型,但 interface 无法像 type 那样表达基本数据类型、元组、联合类型等等。
关于这个问题,最好从基本的面向对象的区分来入手回答才能更加形象具体
首先,接口本来就是面向对象中出现的名字,其目的就是对事务或者行为抽象的总结提取。因此,接口本来就是用来描述某一个结构的。所以对于对象结构的扩展,接口使用的是extends
专属于面向对象的关键字 。而类型别名仅仅代码一个字面量的别名而已。和面向对象是没有任何实际意义上的关联的,类型别名只是人为的使用了交叉运算符&
来模拟了继承的关系,实现对象结构扩展
其次,接口是面向对象的处理,因此在接口中,我们只能使用结构类型。而类型别名可以是任何类型,可以是基础类型,可以是函数,对象结构, 接口 无法像 类型别名 直接那样表达基础类型和元祖类型
正是由于面向对象,对于接口的声明就应该是抽象的描述,而不应该有复杂的类型逻辑运算,因此接口不能表达联合类型(交叉类型可以通过继承实现),也不应该在接口中去处理类型逻辑 。类型别名则没有这些烦恼,因此如果要做类型运算首选的当然是类型别名
面向对象是对结构的设计,因此对于架构的考虑,接口可能需要对外公布的,所以接口有声明合并的特性 。如果我们设计第三方库,有些类型需要考虑到使用者是否需要扩展,那么我们可以声明成接口。当然,如果不需要的话,直接就是类型别名了
8.1 接口与索引签名(映射)所引发的问题
但是接口的声明合并会引发一个可大可小的问题,稍微不注意,会给我们开发留下坑,要说明这个问题,我们一层层的来剖析,首先看下面的问题:
1 2 3 4 5 6 7 8 9 10 type Params = { id : string } let p : Params = { id : '1' };type A = typeof p
当时类型别名Params 的时候,type A = typeof p
能够很清楚的看到,类型就是字面量{id:'1'}
,但是当换成接口Params 的时候,type A = typeof p
只能看到类型是Params类型,大家有没有思考过这个问题呢?
这是因为,设置类型别名之后,VSCode知道当前的类型是固定的,不会再进行改变,而interface由于有声明合并的存在,并不能确定最后的接口内到底有什么内容,所以仅仅只是显示了接口名而已。
如果我们下面添加一个同名接口,就会出现问题。
接下来看索引签名的一个问题:
1 2 3 4 type MyRecord = { [key : string ]: string age : number }
因为索引签名已经规定了,键是string类型,值是string类型,下面我们要自定义自己的键值和上面规定的就不匹配了
所以,像这样的写法就错误了:
1 2 3 4 5 6 7 8 9 10 type MyRecord = { [key : string ]: string } const record : MyRecord = { name : "jack" , type : "admin" , sex : "男" , age : 20 }
record
前面属性键值都是匹配的,但是age
不匹配,所以报错。
上面两个问题都搞清楚之后,下面的问题就好解决了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface MyInterface { name : string } type MyType = { name : string } const example1 : MyType = { name : 'example2' }const example2 : MyInterface = { name : 'example1' }interface MyRecord { [key : string ]: string } let record : MyRecord = {}record = example1; record = example2;
上面MyInterface
和MyType
明明一模一样,example1
与example2
的值也差不多。但是当example2
赋值给record
的时候,却报错了。
原因其实就是TS不能根据自己的类型推导,确定类型签名与接口的值是匹配的,因为接口存在声明合并的情况,而且索引签名也存在如果出现和索引键值不匹配的情况会报错。
当然这是一个很简单的情况,有时候在不经意之间出现了这个问题,确实有点找不到原因所在,比如有下面的函数:
1 2 function useParams<ParamsOrKey extends string | Record <string , string | undefined >>() { }useParams<MyType >()
Record<string, string | undefined>
这个写法希望大家没有忘记,TS的工具函数,和下面这样写的意思是一样的:
1 2 3 { [key :string ]:string |undefined }
不用管这个函数的泛型参数写的有点复杂,就是简单复刻了react-router
路由的useParams
钩子函数,其实就是接受一个字符串,或者索引类型的键值对。
当然泛型是上面的MyType类型别名的时候没有任何问题,但是如果是接口,直接报错:
1 2 3 function useParams<ParamsOrKey extends string | Record <string , string | undefined >>() { }useParams<MyInterface >()
如果你不知道上面的一些问题,这个错误够你找一天。
9. 类的类型理解升级
在typescript中的类有两个点必须要理解
1、在typescript中,类是结构化类型
2、在typescript中,类即声明值也声明类型,而且类的类型有两种:实例化对象类型与类构造函数类型
9.1 类是结构化类型
与typescript中的其他类型一样,typescript根据结构比较类,与类的名称无关。
类与其他类型是否兼容,要看结构
如果一个常规对象定义了同样的属性和方法,那么也与类兼容
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 class User { constructor (public id: number , public name: string ) { } show (addr: string ) { console .log (addr) } } class Person { constructor (public id: number , public name: string ) { } show (addr: string ) { console .log (this .id + "---" + this .name + "---" + addr) } } function desc (user: User ) { user.show ("成都" ) } const u = new User (1 , 'ruby' );const p = new Person (2 , 'jack' );const a = { id : 1 , name : 'ruby' , show (addr : string ): void { console .log ("hello " + addr) }, } desc (u);desc (p);desc (a);
不过稍微要注意的是,如果类中有private或者protected修饰的字段,情况就不一样了,typescript会触发检查
9.2 类即是值也是类型
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 class MyMap { state : Record <string , string > = {} get (key : string ): string | undefined { return this .state [key] } set (key : string , value : string ): void { this .state [key] = value } values (): string [] { return Object .values (this .state ) } keys (): string [] { return Object .keys (this .state ) } static of (...entries : [string , string ][]): MyMap { const map = new MyMap (); entries.forEach (entry => map.set (entry[0 ], entry[1 ])) return map; } } const m1 = new MyMap ();m1.set ("id" ,"1" ) m1.set ("name" , "jack" ) console .log (m1.get ("name" ));console .log (m1.values ());const m2 = MyMap .of (["name" , "rose" ], ["sex" , "女" ]);console .log (m2.keys ());
注意这句代码:
1 const m1 :MyMap = new MyMap ();
按照上面的写法m1:MyMap
中的MyMap
很明显是类型,new MyMap()
既然已经在操作了,那明显应该是个值。
但是,如果仅仅是这样的话,中间缺失了一环
上面的代码中,m1指MyMap类的一个实例。这个是实例化类型 。我们可以通过m1.xxx
调用对应的属性和方法
而new MyMap()
这里既然表示是个值,那么他就应该有值所对应的类型,而且,对于静态方法和静态属性,我们不是还可以通过MyMap.xxx
去调用吗?
所以,还有一个类型,表示的是MyMap()
这个构造函数的类型
如果你觉得这么描述你不太清楚,我们换一种描述方式:
如果我现在有两个方法,其中一个方法需要的是对象,一个方法是类本身,那这个应该怎么限定?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class User { constructor (public id: number , public name: string ) { } show ( ) { console .log (this .id , this .name ); } } function method1 (target:User ) { console .log (target.id ); console .log (target.name ); target.show (); } function method2 (target: new (...args: [number , string ]) => User ) { const t = new target (1 , "jack" ); } method1 (new User (1 , "jack" ));method2 (User );
构造函数类型的表示:
1 new (...args :any []) => any ;
但是,我们怎么表示MyMap类自身的构造函数类型 呢?可以使用typeof
关键字
1 type MyMapConstructorType = typeof MyMap ;
甚至可以通过typescript给我们预留的工具,通过构造器类型再得到实例化类型:
1 type MyMapInstanceType = InstanceType <MyMapConstructorType >;
当然,我们并看不见这个类型中具体有哪些,我们可以模仿着ES关于类的处理,来模拟一下,比如String,Map这些类,大家可以看一下lib.es5.d.ts
中的源代码
实例化类型的MyMap类型
1 2 3 4 5 6 7 interface MyMap { state : Record <string , string > get (key : string ): string | undefined set (key : string , value : string ): void values (): string [] keys (): string [] }
构造方法类型typeof MyMap
1 2 3 4 5 interface MyMapConstructor { new (): MyMap of (...entries : [string , string ][]): MyMap readonly prototype :MyMap }
所以,对于构造函数类型,我们反过来推导一样成立:
1 2 3 var m : MyMapConstructor ;type M1 = typeof m;type M2 = InstanceType <M1 >;
如果你觉得MyMap复杂了一些,我们简化一下:
1 2 3 4 5 6 7 8 9 10 class Person { constructor (public name: string ) {} } type PersonInstance = InstanceType <typeof Person >const person : PersonInstance = new Person ('jack' )interface User { new (name : string ): User } type UserInstance = InstanceType <User >
其实这就对应着我们在vue的代码中,为模板标注类型 ,可以看到有这样的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script> import {ref} from "vue" import MyComp from "./components/MyComp.vue" ;type MyCompConstructor = typeof MyComp ;type MyCompInstance = InstanceType <MyCompConstructor >;const myComp = ref<MyCompInstance | null >(null )const openModal = ( ) => { myComp.value ?.open (); } </script> <template > <MyComp ref ="myComp" /> </template >
MyComp
实际得到的是vue的配置对象,其实就相当于MyComp.vue.vue
文件中,我们所写在script
中的内容,包括data,methods,生命周期函数等等。
通过typeof MyComp
其实得到了vue组件的构造函数类型,然后再通过InstanceType<MyCompConstructor>
得到真正的vue实例对象类型,有了这样的类型之后,我们才能在声明的变量中访问到MyComp
暴露出来的方法
10. 混入
10.1 JavaScript中的混入
在JavaScript中实现对象的混入很简单,我们经常在使用,一般我们都通过浅拷贝,简单地将一个对象的属性和方法复制到另一个对象中来完成。最常见的做法是使用 Object.assign()
或展开运算符(...
)来实现
10.1.1 对象的混入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const canEat = { eat : function ( ) { console .log ("eating" ); }, }; const canWalk = { walk : function ( ) { console .log ("walking" ); }, }; const person = { ...canEat, ...canWalk };person.eat (); person.walk ();
当然这是基于对象的,如果是基于类的,也可以,基本也是两种做法,最简单的,其实就是把上面的person转换为class,然后把对象上的属性加入到class的原型上去
10.1.2 通过原型混入
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 const canEat = { eat : function ( ) { console .log ("eating" ); }, }; const canWalk = { walk : function ( ) { console .log ("walking" ); }, }; class Person { constructor (name ) { this .name = name; } } Object .assign (Person .prototype , canEat, canWalk);const person = new Person ("Alice" );person.eat (); person.walk ();
10.1.3 通过闭包高阶函数混入
这种做法其实在以前React使用类组件的时候,高阶组件经常见到
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 function withEating (Class ) { return class extends Class { eat ( ) { console .log ("eating" ); } }; } function withWalking (Class ) { return class extends Class { walk ( ) { console .log ("walking" ); } }; } class Person { constructor (name ) { this .name = name; } } const EatingAndWalkingPerson = withWalking (withEating (Person ));const person = new EatingAndWalkingPerson ("Bob" );person.eat (); person.walk ()
10.2 Typescript实现混入
如果你理解了javascript混入,typescript的混入没有什么区别。我们通过typescript来实现一下高阶函数的混入。
这里有一个疑问需要解决一下,高阶函数的参数需要的是一个构造函数类型,如何声明一个构造函数类型呢?
要声明一个构造函数类型很简单,如下:
1 type ClassConstructor = new (...args : any []) => any ;
其中,最关键的就是new
这个关键字,表示该签名是一个构造函数,意味着使用这个类型的函数可以通过 new
关键字被实例化。
当然我们可以加入泛型:
1 type Constructor <T = {}> = new (...args : any []) => T;
那接下来的事情就简单了:
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 type Constructor <T = any > = new (...args : any []) => T;function Timestamped <TBase extends Constructor >(Base : TBase ) { return class extends Base { timestamp = Date .now (); }; } function Printable <TBase extends Constructor >(Base : TBase ) { return class extends Base { print ( ) { console .log (this ); } }; } class MyClass { constructor (public name: string ) {} } const TimestampedPrintableMyClass = Timestamped (Printable (MyClass ));const example = new TimestampedPrintableMyClass ("test" );console .log (example.name ); console .log (example.timestamp ); example.print ();
11. 构造函数相关类型工具
ConstructorParameters<Type>
从构造函数类型中获取构造函数参数的元组类型
1 2 3 4 5 class User { constructor (public id:number , public name: string ){} } type ConstructorParamsType1 = ConstructorParameters <typeof User >;
对于ConstructorParameters
的实现其实和之前的ReturnType
十分类似,只不过现在我们需要构造函数类型去进行处理
1 2 3 type MyConstructorParams <T extends abstract new (...args : any []) => any > = T extends abstract new (...args : infer R) => any ? R : never ;type ConstructorParamsType2 = MyConstructorParams <typeof User >;
我们可以扩展一个场景实现一下效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Book { title : string ; content?: string ; constructor (title: string ) { this .title = title; } } class CreateInstance <T extends new (...args : any []) => any > { private ClassConstructor : T; constructor (classConstructor: T ) { this .ClassConstructor = classConstructor; } getInstance (...args : ConstructorParameters <T>): InstanceType <T> { return new this .ClassConstructor (...args); } } const instanceCreator = new CreateInstance (Book );const book = instanceCreator.getInstance ("Typescript类型全解" );
另外和构造函数类型直接相关的就是InstanceType<Type>
类型工具了,这个我们之前就用过了,可以通过构造函数类型得到实例对象类型。
1 2 3 4 5 class User { constructor (public id:number , public name: string ){} } type U = InstanceType <typeof User >;
要去实现这个类型工具也非常简单:
1 2 3 type MyInstanceType <T extends abstract new (...args : any []) => any > = T extends abstract new (...args : any []) => infer R ? R : never ;type InstanceUser = MyInstanceType <typeof User >;
再强调一句:typeof User,这里的User代表的是值
当然,可能会有些同学会思考,那么在TS中,可以直接写构造函数吗?
有这个思考,说明你对类型系统的认知还不够明确!
函数是具有二义性的,因此ES6中才有了箭头函数和类的区分,那我们TS中,如果你要使用类,别再考虑什么构造函数的写法,这是不正确的。要使用类,我们就应该使用class。要使用函数,那么这个函数就是一个普通函数。
不过这个思考非常有趣,我们可以接着这个错误思考,再来认知一下类的类型
1 2 3 4 5 function Person (id: number , name: string , age: number ) { this .id = id; this .name = name; this .age = age; }
首先你这么写,在TS中就会直接报错,至少我们先把this
的严格检查去掉noImplicitThis
设置为false
但是这个我们在js中认知的构造函数,还不能直接new
1 const person = new Person (1 , 'John Doe' , 30 );
会告知你:其目标缺少构造签名的 “new” 表达式隐式具有 “any” 类型
因为不知道new的到底是什么,我们可以使用简单的断言
1 const person = new (Person as any )(1 , 'John Doe' , 30 );
来屏蔽这个错误,也能去掉严格检查的noImplicitAny
设置为false
但是就算如此,我们得到的person,也仅仅是一个any
类型的值,所以是没有任何TS提示的。
而且最关键的,如果我们使用InstanceType,试图通过这个工具帮我们找到实例对象类型也是徒劳的。
1 type PersonInstance = InstanceType <typeof Person >;
会告知你:类型“(id: number, name: string, age: number) => void”提供的内容与签名“new (…args: any): any”不匹配
因为typeof Person获取的仅仅就只是一个函数类型,而不会是一个构造函数类型
我们只能人为的构造类型去填补这些缺陷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person (id: number , name: string , age: number ) { this .id = id; this .name = name; this .age = age; } interface Person { id : number ; name : string ; age : number ; } type PersonConstructor = new (id : number , name : string , age : number ) => Person ;type PersonInstance = InstanceType <PersonConstructor >;const person :PersonInstance = new (Person as any )(1 , 'John Doe' , 30 );
除了ConstructorParameters<Type>
和InstanceType<Type>
这两个类型工具之外,再给大家介绍一个常用的类型工具Awaited<Type>
Awaited<Type>
可以用来获取Promise
中的类型(如await、then方法返回的被Promise包裹的数据的类型)。
其中Promise是我们常用的异步类,本身就有Promise
类型和PromiseConstructor
类型
1 type T = Awaited <Promise <Person >>;
Awaited
在我们处理异步场景的时候非常有用。
比如有时候我们从第三方库就仅仅能获取接口,并且这个接口可能还返回的是一个Promise类型,那么我们就可以通过Awaited
帮我们获取具体函数返回的Promise
类型的泛型,也就是真正的数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface User { id : number ; firstName : string ; lastName : string ; age : number ; } async function fetchUser ( ): Promise <User > { const data = await fetch ( "https://mock.mengxuegu.com/mock/65b1f3d1c4cd67421b34cd0c/mock_ts/user" ).then ((res ) => { return res.json (); }); return data; }
比如上面的fetchUser()
是一个第三方库的函数,如果我们希望获取真正返回的数据User
的类型,就可以使用Awaited
1 2 3 4 5 6 7 8 type UserFetch = Awaited <ReturnType <typeof fetchUser>>const user : UserFetch = { id : 1 , firstName : "yuan" , lastName : "jin" , age : 18 , }
1 2 3 4 5 6 type Awaited <T> = T extends null | undefined ? T : T extends object & { then (onfulfilled : infer F, ...args : infer _): any ; } ? F extends ((value: infer V, ...args: infer _ ) => any ) ? Awaited <V> : never : T;
也就是说,Awaited可以深层嵌套,如果没有Promise的话,就直接返回类型:
1 2 3 4 5 6 7 8 9 type A = Awaited <Promise <string >>; type B = Awaited <Promise <Promise <number >>>; type C = Awaited <boolean | Promise <number >>; type D = Awaited <number >; type E = Awaited <null >;
那如果,我希望传入的如果不是Promise
类型就直接报错呢?我们完全可以将Awaited
再封装一下,使用PromiseLike
1 2 3 4 5 6 7 type MyAwaited <T extends PromiseLike <any >> = Awaited <T>;type F = MyAwaited <number >; type G = MyAwaited <null >;