TS进阶——类型体操(二)

640 阅读19分钟

前言

书接上文,前面咱们了解了TS的简单基础类型以及如何上手,相信各位小伙伴都记住啦,今天咱们来了解学一下TS的各种类型体操。

前面咱们说到,TypeScript(TS)就是为 JS 提供了一个类型限定,它的主要目的是让 js 代码更加健壮和可维护,在学习 TS 时,其中最重要的一环就是理解并熟练运用类型系统。而“类型体操”则是通过各种 TS 类型操作技巧,来实现复杂的类型转换和推导,从而充分利用 TS 的类型系统,大家取名的体操这个词还是非常形象的,说明是可以玩出花来的

TS给JS带来了什么?

一、关于类型

ts 最大特性就是为 js 添加了类型检查系统以及类型规范,能在编译阶段提前将类型 bug 暴露出来,而非代码运行时,包括 ts 一些新的关键字,花里胡哨的东西最终目的都是在于此处

let num: number = "hello"; 
// Error: Type 'string' is not assignable to type 'number'

二、关于工程化

只要能促进团队开发效率的事务都可以被称之为工程化,在大型项目中,ts 很好的帮助我们约束了数据类型,让团队开发者遵循同一套代码规范,ts 通过类型检查确保代码质量,使得在持续集成(Continuous Integration, CI)持续交付/部署(Continuous Delivery/Deployment, CD) 过程中更容易发现问题,从而减少上线后的故障风险

正文-TS的高级类型

一、class类

1.1 class

TS全面支持ES2015中引入的class关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)

class Person{
}
const p=new Person()

1、根据TS中的类型推论,可以知道Person类的实例对象p的类型也是Person.

2、TS中的class,不仅提供了class的语法功能,也作为一种类型存在。

1.2:构造函数

class Person{
age:number
gender:string
	constructor(age:number,gender:string){
	this.age=age
	this.gender=gender
	}
}

1、成员初始化(比如,age:number)后,才可以通过this.age来访问实例成员。

2、需要为构造函数指定类型注解,否则会被隐式的推断为any;构造函数不需要返回值类型。

1.3:实例方法

class Pooint{
x=1
y=2
scale(n:number){
this.x*=n
this.y*=n
}
}

方法的类型注解(参数和返回值)与函数用法相同

1.4:类继承

类继承的两种方式:1、extends(继承父类)2、implements(实现接口)

1、extends(继承父类)

说明:js中只有extends,而implements是TS提供的。

class Animal{
move(){console.log('Moving along')}
}
class Dog extends Animal{
bark(){console.log('汪!')}
}
const dog=new Dog()

1、通过extends关键字实现继承

2、子类Dog继承父类Animal,则Dog的实例对象dog就同时具有父类Animal和子类Dog的所有属性和方法。

2、implements(实现接口)
interface Singable{
sing():void
}
class Person implements Singable{
sing(){
console.log('llll')
}
}

1、通过implements关键字让class实现接口。

2、Person类实现接口Singable 意味着,Person类中必须提供Singable接口中指定的所有方法和属性。

1.5可见性修饰符

类成员可见性:可以使用TS来控制class的方法或属性对于class外的代码是否可见。

可见性修饰符包括1、public(公有的),2、protected(受保护的),3、private(私有的)

1、public(公有的)

表示公有的公开的,公有成员可以被任何地方访问,默认可见性

class Animal{public move(){console.log('Moving along')}}

在类属性或方法前面添加public关键字,来修饰该属性或方法是公有的。

因为public是默认可见性,所以可以直接省略

2、protected(受保护的)

仅对其声明所在类和子类中(非实例对象)可见

class Animal{
protected move(){console.log('Moving along')}
}
class Dog extends Animal{
bark(){console.log('汪!')
this.move()
}
}

注、1、在类属性或方法前面添加protected关键字,来修饰属性或方法是受保护的。

2、在子类的方法内部可以通过this来访问父类中受保护的成员,但是对实例不可见!

3、private(私有的)

表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的。

class Animal{
private move(){console.log('Moving along!')}
walk(){
this.move()
}
}

私有的属性或者方法只在当前类中可见,对子类和实例对象也都是不可见的!

1.6readonly只读修饰符

除了可见性修饰符之外,还有一个常见修饰符就是这个

readonly 表示只读,用来防止在构造函数之外对属性进行赋值。

class Person{
readonly age: number=18
constructor(age:number){
this.age=age
}
}

1、使用readonly关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法。

2、注意:属性age后面的类型注解(比如,此处的number)如果不加,则age的类型为18(字面量类型)

只要是readonly来修饰的属性,必须手动提供明确的类型

3、接口或者{}表示的对象类型,也可以使用readonly

二、类型兼容型

两种类型系统:1、structurlType System(结构化类型系统)2、Nominal Type System (标明类型系统)

TS采用的是结构化类型系统,也叫做duck typing(鸭子类型),类型检查关注的是值所具有的形状。也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。

class Point {x:number; y:number}
class Point2D {x:number;y:number}
const p:Point=new Point2D()

1、Point和Point2D是两个名称不同的类。

2、变量P的类型被显示的标注为Point类型,但是,它的值却是Point2D的实例,并且没有类型错误。

3、因为TS是结构化类型系统,只检查Point和Point2D的结构是否相同(相同,都具有x和y两个属性,属性类型也相同)

4、但是,如果在Nominal Type System中(比如C#、java)他们是不同的类,类型无法兼容

对象之间的兼容性

在结构化类型系统中,如果两个对象具有相同的形状,则认为他们属于同一类型,这种说法并不准确。

更准确的说法:对于对象类型来说,y的成员至少与x相同,则x兼容y(成员多的可以赋值给少的)

class Point {x:number;y:number}
class Point3D{x:number;y:number;z:number}
const p:Point=new Point3D()

1、Point3D的成员至少与Point相同,则Point兼容Point3D。

2、所以,成员多的Point3D可以赋值给成员少的Point

接口兼容性

除了class之外,TS中的其他类型也存在相互兼容的情况,包括接口兼容性

接口之间的兼容性,类似于class。并且class和interface之间也可以兼容

interface Point {x:number;y:number}
interface Point2D {x:number;y:number}
let p1:Point
let p2:Point2D=p1

interface Point3D{x:number;y:number;z:number}
let p3:Point3D
p2=p3

函数兼容性

函数之间的兼容性比较复杂,需要考虑:1参数个数 2参数类型 3返回值类型

1参数个数

参数多的兼容参数少的,(或者说,参数少的可以赋值给多的)

type F1 =(a:number)=>void
type F2=(a:number,bnumber)=>void
let f1:F1
let f2:F2=f1

const arr=['a','b''c']
arr.forEach(()=>{})
arr.forEach((item)=>{})

1、参数少的可以赋值给参数多的,所以,f1可以赋值给f2

2、数组forEach方法的第一个参数是回调函数,该示例中类型为(value:string,index:number,arry:string[])=>void

3、在js中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了TS中函数之间的兼容性。

4、并且因为回调函数是有类型的,所以TS会自动推导出参数item\index\array的类型

2参数类型

相同位置的参数类型要相同或兼容

image.png

1、注意,此处与前面讲到的接口兼容性冲突

2、技巧:将对象拆开,把每一个属性看做一个个参数,则,参数少的(f2)可以赋值给参数多的(f3)

3返回值类型

返回值类型只需关注返回值类型本身即可

type F5=()=>string
type F6=()=>string
let f5:F5
let f6:F6=F5

type F7=()=>{name:string}
type F8=()=>{name:string;age:number}
let f7:F7
let f8:F8
f7=f8

1、如果返回值类型是原始类型,此时两个类型要相同,比如,左侧类型F5和F6

2、如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如,右侧类型F7和F8.

三、交叉类型

交叉类型(&):

功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)

interface Person {name: string}
interface Contact {phone:string}
type PersonDetail=Person & Contact
let obj: PersonDetail={
name: 'jack',
phone: '133...'
}

使用交叉类型后,新的类型PersonDetail就同时具备了Person和Contact的所有属性类型。就相当于

type PersonDetail={ name: string; phone:string}

交叉类型(&)和接口继承(extends)的对比:

~相同点:都可以实现对象类型的组合

~不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。

image.png

上述代码:接口继承会报错(类型不兼容);交叉类型没有错误,可以简单的理解为:

fn:(value: string|number)=>string

四、泛型和keyof

泛型是可以再保证类型安全的前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class中

例如

需求:创建一个id函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)

function id(value:number):number{return value}

比如,id(10)调用以上函数就会直接返回10本身。但是,该函数只接受数值类型,无法用于其他类型。

为了能让函数能够接受任意类型,可以将参数类型修改为any。但是,这样就失去了TS的类型保护,类型不安全。

function id(value:any):any {return value}

泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。

创建泛型函数

function id<Type>(value: Type):Type{return value}

1、语法:在函数名称的后面添加<>(尖括号),尖括号中添加类型变量,比如此处的Type.

2、类型变量Type,是一种特殊类型的变量,它处理类型而不是值。

3、该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)

4、因为Type是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型

5、类型变量Type,可以是任意合法的变量名称。

调用泛型函数

function id<Type>(value: Type):Type{return value}
const num=id<number>(10)
const str=id<string>('a')

1、语法:在函数名称的后面添加<>(尖括号),尖括号中指定具体的类型,比如此处的number.

2、当传入类型number后,这个类型就会被函数声明时指定的类型变量Type捕获到。

3、此时,Type的类型就是number,所以,函数id参数和返回值的类型也都是number。

同样,如果传入类型string,类型id参数和返回值的类型就都是string

这样,通过泛型就做到了让id函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全。

简化调用泛型函数

1、在调用泛型函数时,可以省略<类型>来简化泛型函数的使用

2、此时,ts的内部会采用一种叫做类型参数判断的机制,来根据传入的实参自动推断出类型变量Type的类型。

当编译器无法推断类型或者推断的类型不准确时,就需要显示的传入类型参数。

泛型约束

默认情况下,泛型函数的类型变量Type可以代表多个类型,这可能导致无法访问任何属性。比如,id('a')调用函数时获取参数的长度

image.png Type可以代表任意类型,无法保证一定存在length属性,比如number类型就没有length.此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)

主要有以下两种方式:

1、指定更加具体的类型 2 、添加约束

1、指定更加具体的类型

function id<type>(value: type[]):Type[]{
console.log(value.length)
return value
}

比如,将类型修改为Type[](Type类型的数组),因为只要是数组就一定存在length属性,因此就可以访问了。

2 、添加约束

image.png

1、创建描述约束的接口ILength,该接口要求提供length属性。

2、通过extends关键字使用该接口,为泛型(类型变量)添加约束

3、该约束表示:传入的类型必须具有length属性。

多个泛型变量的约束

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量的约束)

eg: 创建一个函数来获取对象中属性的值:

function getProp<Type,key extends keyof Type>(obj: Type,key:key){
return obj[key]
}

1、添加了第二个类型变量key,两个类型变量之间使用(,)逗号分隔

2、keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。

3、本示例中 keyof Type实际上获取的是person对象所有键的联合类型,也就是'name'|'age'.

4、类型变量key受Type约束,可以理解为:key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性。

泛型接口

接口也可以配合泛型来使用,以增强其灵活性,增强其复用性。

interface IdFunc<Type>{
id:(value:Type)=>Type
ids:()=>Type
}

let obj: IdFunc<number>={
id(value){return value}
ids(){return [1,3,5]}
}

1、在接口名称的后面添加<类型变量>,那么,这个接口就变成了泛型接口。

2、接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。

3、使用泛型接口时,需要显示指定具体的类型(比如,此处的IdFunc)

4、此时,id方法的参数和返回值类型都是number; ids方法的返回值类型是number[]

实际上,js中的数组在TS中就是一个泛型接口

image.png

当我们在使用数组时,TS会根据数组的不同类型,来自动将类型变量设置为相应的类型。

技巧:可以通过Ctrl+鼠标左键(mac:option+鼠标左键)来查看具体的类型信息。

泛型类

class也可以配合泛型来使用

创建泛型类:

class GenericNumber<NumType>{
defaultValue: Number
add:(x:Number,y:NumType)=>NumType
}

1、类似于泛型接口,在class名称后面添加<类型变量>,这个类就变成了泛型类

2、此处的add方法,采用的是箭头函数类型的类型书写方式

泛型工具类型

泛型工具类型:TS内置了一些常用的工具类型,来简化TS中的一些常见操作。

她们都是基于泛型实现的(适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用

Partial

泛型工具类型-Partial用来构造(创建)一个类型,将Type的所有属性设置为可选。

interface Props{
id:string
children:number[]
}
type PartialProps=Partial<Props>

结构出来的新类型PartialProps结构和Props相同,但所有属性都变为可选的。

Readonly

泛型工具类型-Readonly用来构造(创建)一个类型,将Type的所有属性设置为只读。

interface Props{
id:string
children:number[]
}
type ReadonlyProps=Readonly<Props>

构造出来的新类型ReadonlyProps结构和Props相同,但所有属性都变为只读的。

Pick

泛型工具类型-Pick<Type,keys> 从Type中选择一组属性来构造新类型

interface Props{
id:string
title:string
children:number[]
}
type PickProps=Pick<Props,'id'|'title'>

1、Pick工具类型有两个类型变量:1表示选择谁的属性2表示选择哪几个属性

2、其中第二个类型变量,如果只选择一个则只传入该属性名即可

3、第二个类型变量传入的属性只能是第一个类型变量中存在的属性

Record

泛型工具类型-Record<Keys,Type>构造一个对象类型,属性键为Keys,类型为Type.

type RecordObj=Record<'a'|'b'|'c',string[]>
let obj:RecordObj={
a:['1'],
b:['2'],
c:['3']
}

1、Record工具类型有两个类型变量:1表示对象有哪些属性2表示对象属性的类型

2、构建的新类型对象RecordObj表示:这个对象有三个属性分别是a/b/c,属性值的类型都是string[].

五、索引签名类型

绝大数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型。

使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时就用到索引签名类型了

interface AnyObject{
[key:string]:number
}
let obj:AnyObject={
a:1,
b:2
}

1、使用[key:string]来约束该接口中允许出现的属性名称。表示只要是string类型的属性名称,都可以出现在对象中。

2、这样,对象obj中就可以出现任意多个属性(比如a,b等)

3、key只是一个占位符,可以换成任意合法的变量名称。

4、隐藏的前置知识:JS中对象({})的键是string类型的。

在JS中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型。

并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型。

interface MyArray<T>{
[n:number]:T
}
let arr: MyArray<number>=[1,3,5]

1、MyArray接口模拟原生的数据接口,并使用[n:number]来作为索引签名类型。

2、该索引签名类型表示:只要是number类型的键(索引)都可以出现在数组中或者说数组中可以有任意多个元素。

3、同时也符合数组索引是number类型这一前提。

六、映射类型

映射类型:基于旧类型创建新类型(对象类型),减少重复,提升开发效率。

比如类型PropKeys有x/y/z,另一个类型Type中也有x/y/z,并且Type1中x/y/z的类型相同。

type PropKeys='x'|'y'|'z'
type Type1={x:number; y:number;z:number}

这样书写没错,但x/y/z重复书写了两次,像这种情况,就可以使用映射类型来进行简化。

type PropKeys='x'|'y'|'z'
type Type2={[key in PropKeys]:number}

1、映射类型是基于索引签名类型的,所以,该语法类似于索引签名类型,也使用[]

2、key in PropKeys表示key可以是PropKeys联合类型中的任意一个,类似于forin(let k in obj)

3、使用映射类型创建的新对象类型Type2和Type1结构完全相同

4、注意:映射类型只能在类型别名中使用,不能在接口中使用。

根据对象创建

映射类型除了根据联合类型创建新类型外,还可根据对象类型来创建:

type Props={a:number;b:string;c:boolean}
type Type3={[key in keyof Props]:number}

1、首先,先执行keyof Props获取到对象类型Props中所有键的联合类型,即 'a'|'b'|'c'

2、然后,key in...就表示key可以是Props中所有键名称中的任意一个

分析映射类型partial的实现原理

实际上,前面咱们聊到的泛型工具类型都是基于映射类型实现的

比如,Partial的实现

type Partial<T>={
[P in key T]?:T[p]
}

type Props={a:number;b:string;c:boolean}
type ParticalProps=Partial<Props>

1、keyof T即keyof Props表示获取Props的所有的键,也就是:'a'|'b'|'c'

2、在[]后面添加?(问号)表示将这些属性变为可选的,以此来实现Partial的功能。

3、冒号后面的T[p]表示获取T中每个键对应的类型。比如,如果是'a'则类型是number;如果是'b'则类型是string.

4、最终,新类型PartialProps和旧类型Props结构完全相同,只是让所有类型都变为可选的了。

面试官:手写Readonly,Partial,Pick

// 手写 Readonly

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 手写 Partial

type MyPartial<T> = {
    [P in keyof T]?: T[P]
}

// 手写 Pick

type MyPick<T, K extends keyof T> = {
    [P in K]: T[P];
};

前面两个很好理解,这里重点看下 Pick

pick 的第二个参数 K 就是个来自 T 所有属性字面量的联合类型或者单个属性键,keyof T 获取 T 的所有属性键,返回一个联合类型,若 T 是 { name: string; age: number; },那么 keyof T 就是 'name' | 'age'K extends keyof T 是一个泛型约束,表示 T 必须是 keyof T 的子类型,也就是 'name' | 'age' 的子类型。

七、索引查询类型

刚刚用到的T[p]语法,在TS中叫做索引查询(访问)类型

作用:用来查询属性的类型。

type Props={a:number;b:string;c:boolean}

type TypeA=Props['a']

Prop['a']表示查询类型Props中属性'a'对应的类型number.所以,TypeA的类型为number.

注意:[]中的属性必须存在于被查询类型中,否则会报错!

结尾

ts 相关内容还是非常多的,所以调侃为类型体操,不过掌握本文的 ts进阶知识和上文的基础类型让你上手项目以及阅读源码再或者是应对面试应该是没问题的。

今天的分享就到这里结束啦,如果有任何问题或建议,欢迎指出,另外,有不懂之处欢迎在评论区留言。如果觉得文章对你的学习有所帮助,还请 关注、点赞、收藏 一键三连,感谢支持!