Appearance
认识泛型
软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性:
- 比如我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作
- 但是对于参数的类型是否也可以参数化呢?
什么是类型的参数化?
- 我们来提一个需求:封装一个函数,传入一个参数,并且返回这个参数
如果我们是TypeScript的思维方式,要考虑这个参数和返回值的类型需要一致:
ts
function foo(arg: number): number {
return arg
}上面的代码虽然实现了,但是不适用与其他类型,比如string、boolean、Person等类型:
ts
function foo(arg: any): any {
return arg
}泛型实现类型参数化
虽然any是可以的,但是定义为any的时候,我们其实已经丢失了类型信息:
- 比如我们传入的是一个number,那么我们希望返回的可不是any类型,而是number类型
- 所以,我们需要在函数中可以捕获到参数的类型是number,并且同时使用它来作为返回值的类型
我们需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值:
ts
function foo<Type>(arg: Type): Type {
return arg
}这里我们可以使用两种方式来调用它:
方式一:通过 <类型> 的方式将类型传递给函数;
方式二:通过类型推导(type argument inference),自动推到出我们传入变量的类型:
在这里会推导出它们是 字面量类型的,因为字面量类型对于我们的函数也是适用的
tsx
// 1.理解形参和实例参数化, 但是参数的类型是固定的
// function foo(name: string, age: number) {
// }
// foo("why", 19)
// foo("kobe", 30)
// 2.定义函数: 将传入的内容返回
// number/string/{name: string}
function bar<Type>(arg: Type): Type {
return arg
}
// 2.1. 完整的写法
const res1 = bar<number>(123)
const res2 = bar<string>("abc")
const res3 = bar<{name: string}>({ name: "why" })
// 2.2. 省略的写法
const res4 = bar("aaaaaaaaa")
const res5 = bar(11111111)useState的练习
ts
// 元组: useState函数
function useState<Type>(initialState: Type): [Type, (newState: Type) => void] {
let state = initialState
function setState(newState: Type) {
state = newState
}
return [state, setState]
}
// 初始化count
const [count, setCount] = useState(100)
const [message, setMessage] = useState("Hello World")
const [banners, setBanners] = useState<any[]>([])泛型的基本补充
当然我们也可以传入多个类型:
ts
function foo<T, E>(a1: T,a2: E) {
}平时在开发中我们可能会看到一些常用的名称:
- T:Type的缩写,类型
- K、V:key和value的缩写,键值对
- E:Element的缩写,元素
- O:Object的缩写,对象
ts
function foo<T, E>(arg1: T, arg2: E) {
}
foo(10, 20)
foo(10, "abc")
foo<string, { name: string }>("abc", { name: "why" })泛型接口
在定义接口的时候我们也可以使用泛型:
ts
interface IKun<Type = string> {
name: Type
age: number
slogan: Type
}
const kunkun: IKun<string> = {
name: "why",
age: 18,
slogan: "哈哈哈"
}
const ikun2: IKun<number> = {
name: 123,
age: 20,
slogan: 666
}
const ikun3: IKun = {
name: "kobe",
age: 30,
slogan: "坤坤加油!"
}泛型类
我们也可以编写一个泛型类:
ts
class Point<Type = number> {
x: Type
y: Type
constructor(x: Type, y: Type) {
this.x = x
this.y = y
}
}
const p1 = new Point(10, 20)
console.log(p1.x)
const p2 = new Point("123", "321")
console.log(p2.x)泛型约束(Generic Constraints)
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:
- 比如string和array都是有length的,或者某些对象也是会有length属性的
- 那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢?
ts
interface ILength {
length: number
}
// 1.getLength没有必要用泛型
function getLength(arg: ILength) {
return arg.length
}
const length1 = getLength("aaaa")
const length2 = getLength(["aaa", "bbb", "ccc"])
const length3 = getLength({ length: 100 })
// 2.获取传入的内容, 这个内容必须有length属性
// Type相当于是一个变量, 用于记录本次调用的类型, 所以在整个函数的执行周期中, 一直保留着参数的类型
function getInfo<Type extends ILength>(args: Type): Type {
return args
}
const info1 = getInfo("aaaa")
const info2 = getInfo(["aaa", "bbb", "ccc"])
const info3 = getInfo({ length: 100 })
// getInfo(12345)
// getInfo({})泛型参数使用约束
在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)
你可以声明一个类型参数,这个类型参数被其他类型参数约束
举个栗子:我们希望获取一个对象给定属性名的值
- 我们需要确保我们不会获取 obj 上不存在的属性
- 所以我们在两个类型之间建立一个约束
ts
// 传入的key类型, obj当中key的其中之一
interface IKun {
name: string
age: number
}
type IKunKeys = keyof IKun // "name"|"age"
function getObjectProperty<O, K extends keyof O>(obj: O, key: K){
return obj[key]
}
const info = {
name: "why",
age: 18,
height: 1.88
}
const name = getObjectProperty(info, "name")映射类型(Mapped Types)
有的时候,一个类型需要基于另外一个类型,但是你又不想拷贝一份,这个时候可以考虑使用映射类型
- 大部分内置的工具都是通过映射类型来实现的
- 大多数类型体操的题目也是通过映射类型完成的
映射类型建立在索引签名的语法上:
- 映射类型,就是使用了 PropertyKeys 联合类型的泛型
- 其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型
ts
// TypeScript提供了映射类型: 函数
// 映射类型不能使用interface定义
// Type = IPerson
// keyof = "name" | "age"
type MapPerson<Type> = {
// 索引类型以此进行使用
[aaa in keyof Type]: Type[aaa]
// name: string
// age: number
}
interface IPerson {
name: string
age: number
}
// 拷贝一份IPerson
// interface NewPerson {
// name: string
// age: number
// }
type NewPerson = MapPerson<IPerson>映射修饰符(Mapping Modifiers)
在使用映射类型时,有两个额外的修饰符可能会用到:
- 一个是 readonly,用于设置属性只读
- 一个是 ? ,用于设置属性可选
你可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀
ts
type MapPerson<Type> = {
readonly [Property in keyof Type]?: Type[Property];
};
interface IPerson {
name: string;
age: number;
height: number;
address: string;
}
type IPersonOptional = MapPerson<IPerson>;
const p: IPersonOptional = {};ts
type MapPerson<Type> = {
// 去掉readonly,去掉?,默认都是+
-readonly [Property in keyof Type]-?: Type[Property]
}
interface IPerson {
name: string
age?: number
readonly height: number
address?: string
}
//
type IPersonRequired = MapPerson<IPerson>
const p: IPersonRequired = {
name: "why",
age: 18,
height: 1.88,
address: "广州市"
}拷贝出来的IPersonRequired相当于是去掉所有的readonly和?,也就是下面这样
ts
type IPersonRequired = {
name: string;
age: number;
height: number;
address: string;
}内置工具和类型体操
类型系统其实在很多语言里面都是有的,比如Java、Swift、C++等等,但是相对来说TypeScript的类型非常灵活:
这是因为TypeScript的目的是为JavaScript添加一套类型校验系统,因为JavaScript本身的灵活性,也让TypeScript类型系统
不得不增加更复杂的功能以适配JavaScript的灵活性;
所以TypeScript是一种可以支持类型编程的类型系统;
这种类型编程系统为TypeScript增加了很大的灵活度,同时也增加了它的难度:
如果你不仅仅在开发业务的时候为自己的JavaScript代码增加上类型约束,那么基本不需要太多的类型编程能力;
但是如果你在开发一些框架、库,或者通用性的工具,为了考虑各种适配的情况,就需要使用类型编程;
TypeScript本身为我们提供了类型工具,帮助我们辅助进行类型转换(前面有用过关于this的类型工具)。
很多开发者为了进一步增强自己的TypeScript编程能力,还会专门去做一些类型体操的题目:
https://github.com/type-challenges/type-challenges
https://ghaiklor.github.io/type-challenges-solutions/en/
我们课堂上会学习TypeScript的编程能力的语法,并且通过学习内置工具来练习一些类型体操的题目。
axios请求封装
主要是封装了全局拦截器,还有每个实例的拦截器,以及单次请求的拦截器。
request/index.ts
typescript
import axios from "axios"
import type { HYRequestConfig, CoverAxiosInstance } from "./type"
// 拦截器: 蒙版Loading/token/修改配置
/**
* 两个难点:
* 1.拦截器进行精细控制
* > 全局拦截器
* > 实例拦截器
* > 单次请求拦截器
*
* 2.响应结果的类型处理(泛型)
*/
class HYRequest {
instance: CoverAxiosInstance
// request实例 => axios的实例
constructor(config: HYRequestConfig) {
this.instance = axios.create(config)
// 每个instance实例都添加拦截器
this.instance.interceptors.request.use(config => {
// loading/token
console.log("全局请求成功的拦截")
return config
}, err => {
console.log("全局请求失败的拦截")
return err
})
this.instance.interceptors.response.use(res => {
console.log("全局响应成功的拦截")
return res.data
}, err => {
console.log("全局响应失败的拦截")
return err
})
// 针对特定的hyRequest实例添加拦截器
this.instance.interceptors.request.use(
config.interceptors?.requestSuccessFn,
config.interceptors?.requestFailureFn
)
this.instance.interceptors.response.use(
config.interceptors?.responseSuccessFn,
config.interceptors?.responseFailureFn
)
}
// 封装网络请求的方法
// T => IHomeData
request<T = any>(config: HYRequestConfig<T>) {
// 单次请求的成功拦截处理
if (config.interceptors?.requestSuccessFn) {
config = config.interceptors.requestSuccessFn(config)
}
// 返回Promise
return new Promise<T>((resolve, reject) => {
this.instance.request<any, T>(config).then(res => {
// 单词响应的成功拦截处理
if (config.interceptors?.responseSuccessFn) {
res = config.interceptors.responseSuccessFn(res)
}
resolve(res)
}).catch(err => {
reject(err)
})
})
}
get<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: "GET" })
}
post<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: "POST" })
}
delete<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: "DELETE" })
}
patch<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: "PATCH" })
}
}
export default HYRequestrequest/type.ts
注意:
axios最新版本中请求拦截器的类型已经从AxiosRequestConfig变成了InternalAxiosRequestConfig,
InternalAxiosRequestConfig又要求headers必传,因此我们重新改变实例的类型,请求拦截器返回AxiosRequestConfig
typescript
import type { AxiosRequestConfig, AxiosResponse, AxiosInstance, AxiosInterceptorManager } from "axios"
// 针对AxiosRequestConfig配置进行扩展
export interface HYInterceptors<T = AxiosResponse> {
requestSuccessFn?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestFailureFn?: (err: any) => any
responseSuccessFn?: (res: T) => T
responseFailureFn?: (err: any) => any
}
export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: HYInterceptors<T>
}
export interface CoverAxiosInstance extends Omit<AxiosInstance, 'interceptors'> {
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>; // 还是返回AxiosRequestConfig
response: AxiosInterceptorManager<AxiosResponse>;
}
}条件类型(Conditional Types)
很多时候,日常开发中我们需要基于输入的值来决定输出的值,同样我们也需要基于输入的值的类型来决定输出的值的类型。
条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系。
条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression ):
json
SomeType extends OtherType ? TrueType : FalseType;typescript
type IDType = number | string
// 判断number是否是extendsIDType
// const res = 2 > 3? true: false
type ResType = boolean extends IDType? true: false
// 举个栗子: 函数的重载
// function sum(num1: number, num2: number): number
// function sum(num1: string, num2: string): string
// 错误的做法: 类型扩大化
// function sum(num1: string|number, num2: string|number): string
function sum<T extends number | string>(num1: T, num2: T): T extends number? number:string
function sum(num1, num2) {
return num1 + num2
}
const res = sum(20, 30)
const res2 = sum("abc", "cba")
// const res3 = sum(123, "cba")
export {}在条件类型中推断(inter)
在条件类型中推断(Inferring Within Conditional Types)
条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果;
比如我们现在有一个数组类型,想要获取到一个函数的参数类型和返回值类型:
我们可以直接使用内置工具ReturnType和Parameters来获取一个函数的返回值类型和参数类型,但是一些类型体操的题目是需要封装这
些内置工具
typescript
type CalcFnType = (num1: number, num2: string) => number
function foo() {
return "abc"
}
// 总结类型体操题目: MyReturnType
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R? R: never
type MyParameterType<T extends (...args: any[]) => any> = T extends (...args: infer A) => any? A: never
// 获取一个函数的返回值类型: 内置工具
type CalcReturnType = MyReturnType<CalcFnType>
type FooReturnType = MyReturnType<typeof foo>
// type FooReturnType2 = MyReturnType<boolean>
type CalcParameterType = MyParameterType<CalcFnType>
export {}分发条件类型(Distributive Conditional Types)
当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的(distributive)
如果我们在 ToArray 传入一个联合类型,这个条件类型会被应用到联合类型的每个成员:
当传入string | number时,会遍历联合类型中的每一个成员;
相当于ToArray<string> | ToArray<number>;
所以最后的结果是:string[] | number[];
typescript
type toArray<T> = T extends any? T[]: never
// number[]|string[]
type NumArray = toArray<number>
// number[]|string[] 而不是 (number|string)[]
type NumAndStrArray = toArray<number|string>Partial<Type>
用于构造一个Type下面的所有属性都设置为可选的类型
typescript
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
type HYPartial<T> = {
[P in keyof T]?: T[P]
}
// IKun都变成可选的
type IKunOptional = HYPartial<IKun>
export {}Required<Type>
用于构造一个Type下面的所有属性全都设置为必填的类型,这个工具类型跟 Partial 相反。
typescript
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
type HYRequired<T> = {
[P in keyof T]-?: T[P]
}
// IKun都变成必填的
type IKun2 = Required<IKun>
export {}Readonly<Type>
用于构造一个Type下面的所有属性全都设置为只读的类型,意味着这个类型的所有的属性全都不可以重新赋值。
typescript
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
type HYReadonly<T> = {
readonly [P in keyof T]: T[P]
}
// IKun都变成只读的
type IKun2 = HYReadonly<IKun>
export {}Record<Keys, Type>
用于构造一个对象类型,它所有的key(键)都是Keys类型,它所有的value(值)都是Type类型。
typescript
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
// name | age | slogan
type keys = keyof IKun
type Res = keyof any // => number|string|symbol
// 确实keys一定是可以作为key的联合类型
type HYRecord<Keys extends keyof any, T> = {
[P in Keys]: T
}
// 对象类型IKuns,它的key是t1,value是IKun
type t1 = "上海" | "北京" | "洛杉矶"
type IKuns = HYRecord<t1, IKun>
const ikuns: IKuns = {
"上海": {
name: "xxx",
age: 10
},
"北京": {
name: "yyy",
age: 5
},
"洛杉矶": {
name: "zzz",
age: 3
}
}
export {}Pick<Type, Keys>
用于构造一个类型,它是从Type类型里面挑了一些属性Keys
typescript
interface IKun {
name: string
age: number
slogan?: string
}
// 确实keys一定是可以作为key的联合类型
type HYPick<T, K extends keyof T> = {
[P in K]: T[P]
}
// 从IKun中挑出一些keys
type IKuns = HYPick<IKun, "slogan"|"name">
export {}Omit<Type, Keys>
用于构造一个类型,它是从Type类型里面过滤了一些属性Keys
typescript
interface IKun {
name: string
age: number
slogan?: string
}
// 确实keys一定是可以作为key的联合类型
type HYOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never: P]: T[P]
}
// IKun中过滤掉slogan和name,只剩下age
type IKuns = HYOmit<IKun, "slogan"|"name">
export {}Exclude<UnionType, ExcludedMembers>
用于构造一个类型,它是从UnionType联合类型里面排除了所有可以赋给ExcludedMembers的类型。
typescript
type IKun = "sing" | "dance" | "rap"
// 确实keys一定是可以作为key的联合类型
type HYExclude<T, E> = T extends E? never: T
// IKun中排除rap,剩下sing和dance
type IKuns = HYExclude<IKun, "rap">
export {}Extract<Type, Union>
用于构造一个类型,它是从Type类型里面提取了所有可以赋给Union的类型。
typescript
type IKun = "sing" | "dance" | "rap"
// 确实keys一定是可以作为key的联合类型
type HYExtract<T, E> = T extends E? T: never
// IKun中提取rap和dance
type IKuns = HYExtract<IKun, "rap"|"dance">
export {}NonNullable<Type>
用于构造一个类型,这个类型从Type中排除了所有的null、undefined的类型。
typescript
type IKun = "sing" | "dance" | "rap" | null | undefined
// 确实keys一定是可以作为key的联合类型
type HYNonNullable<T> = T extends null|undefined ? never: T
// IKun中排除null和undefined
type IKuns = HYNonNullable<IKun>
export {}ReturnType<Type>
用于构造一个含有Type函数的返回值的类型。
typescript
function foo() {
return "abc"
}
// 总结类型体操题目: MyReturnType
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R? R: never
// 获取一个函数的返回值类型: 内置工具
type FooReturnType = MyReturnType<typeof foo>
export {}InstanceType<Type>
用于构造一个由所有Type的构造函数的实例类型组成的类型。
typescript
class Person {}
class Dog {}
// 类型体操
type HYInstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R? R: never
const p1: Person = new Person()
// typeof Person: 构造函数具体的类型
// InstanceType构造函数创建出来的实例对象的类型
type HYPerson = HYInstanceType<typeof Person>
const p2: HYPerson = new Person()
// 构造函数的例子
// 通用的创建实例的工具函数时会用到这个InstanceType
function factory<T extends new (...args: any[]) => any>(ctor: T): HYInstanceType<T> {
return new ctor()
}
const p3 = factory(Person) // p3会是Person构造函数创建出来的实例对应的类型
const d = factory(Dog) // d会是Dog构造函数创建出来的实例对应的类型