概述
泛型,作为TS语言的重点与难点,是我们必须掌握的知识。那什么是泛型呢?
我看了不少的博客,包括官方文档,大佬的文章等,发现对于泛型都没有下一个准确的定义,只是说“类似”、“型如”之类的字眼,官方描述如下:
结合官方和各位大佬的描述,我总结的泛型的定义如下:
在TypeScript(TS)中,泛型是一种工具,它允许在定义函数、类、接口或类型别名时使用类型参数。
这些类型参数可以在实际使用时被具体的类型所替换,从而使得代码更加灵活和可复用。
泛型的本质是通过类型参数建立函数参数和返回值之间的对应关系,增强代码的复用性。
这么说可能还是有些抽象,话不多说,看代码吧,看看代码就懂了。
泛型语法的基本使用
// 1.理解形参和实例参数化, 但是参数的类型是固定的
// function foo(name: string, age: number) {
// }
// foo("Jasmin", 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: "Lee" })
// 2.2. 省略的写法
const res4 = bar("aaaaaaaaa")
const res5 = bar(11111111)
// let message = "Hello World"
export {}
再看张图吧。
泛型实现类型参数化
这里的T,它是一种类型变量(type variable),它作用于类型,而不是值。
这里我们可以使用两种方式来调它:
- 方式一:通过
<类型 >的方式 将类型传递给函数;
foo<string>("abc")
foo<number>(123)
- 方式二:通过
类型推导( type argument inference),自动推导出我们传入变量的类型- 在这里会推导出它们是字面量类型的,因为字面量类型对于我们的函数也是适用的
foo("abc")
foo(123)
泛型的基本补充
// 元组: 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[]>([])
export {}
常见的泛型变量说明
- T:Type的缩写,类型
- K、V:key和value的缩写,键值对的缩写
- E:Element的缩写,元素
- O:Object的缩写,对象
也可以传入多个泛型变量
function foo<T, E>(arg1: T, arg2: E) {
}
foo(10, 20)
foo(10, "abc")
foo<string, { name: string }>("abc", { name: "why" })
export {}
泛型接口与泛型类
泛型接口
// 可以在定义泛型变量时指定一个默认值
interface IKun<Type = string> {
name: Type
age: number
slogan: Type
}
const kunkun: IKun<string> = {
name: "Lee",
age: 18,
slogan: "哈哈哈"
}
const ikun2: IKun<number> = {
name: 123,
age: 20,
slogan: 666
}
const ikun3: IKun = {
name: "kun",
age: 30,
slogan: "哭唧唧,我再也不能给你们唱跳rap了"
}
export {}
泛型类
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)
export {}
泛型约束
有时我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中。比如 string 和array都是有 length属性的,或者某些对象也是会有length属性的。那么只要是拥有length 属性的都可以作为我们参数类型,那么应该如何操呢? 这就要引入我们所说的泛型约束了。看例子吧
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相当于是一个变量, 用于记录本次调用的类型
// 所以在整个函数的执行周期中, 一直保留着参数的类型
// 这里表示是传入的类型必须有length这个属性,也可以有其他属
// 但是必须至少有这个成员
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({})
export {}
泛型参数使用约束
在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)。你可以声明一个类型参数,这个类型参数被其他类型参数约束。
举个栗子:我们希望获取一对象给定属性名的值
- 我们需要确保不会获取 obj上不存在的属性
- 所以我们要在两个类型之间建立一个约束
// 传入的key类型, obj当中key的其中之一
interface IKun {
name: string
age: number
}
// keyof:映射类型语法,可以理解为联合类型的语法糖
// 下一个知识点就会对其进行讲述
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")
export {}
映射类型
有的时候,一个类型需要基于另外一个类型,但是我们又不想拷贝一份,这个时候可以考虑使用映射类型
- 大部分内置的工具都是通过映射类型来实现的
- 大多数类型体操的题目也是通过映射完成的
映射类型建立在索引签名的语法上:
- 映射类型,就是使用了 PropertyKeys联合类型的泛型
- 其中 PropertyKeys多是通过keyof创建,然后循环遍历键名创建一个类型
需要注意的是 映射类型不能使用interface定义
基本使用
// 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>
export {}
映射修饰符
在使用映射类型时,有两个额外的修饰符可能会到:
- 一个是
readonly,用于设置属性只读; - 另一个是
?,用于设置属性可选;
修饰符符号: - 或 +
你可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀相当于使用了 + 前缀
type MapPerson<Type> = {
// 这个映射表达的意思是:
// 1.首先,使用映射修饰符readonly和?将Type中的所有属性设置为只读并且可选的
// 2.然后,又使用修饰符符号 - 将这两个修饰符删除
-readonly [Property in keyof Type]-?: Type[Property]
}
/*
不知道大家会不会有疑问,先添加,后删除,那这不是脱裤子放P -- 多次一举吗?
这不是跟 [Property in keyof Type]: Type[Property] 表达的意思一样吗?
事实上,不是这样的,看下面这个例子你就懂了。
经过MapPerson里的映射`运算`,类型IPerson的所有属性都变为必填,并且可读可写的。
而如果是经过这个: [Property in keyof Type]: Type[Property]
没有任何修饰符和修饰符符号的`运算`,IPerson的各个属性还会保持他们原先的属性。
即name,age,address都是可读写的,但是age和address是可选的
height是只读的
*/
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: "广州市"
}
export {}
内置工具和类型体操
这部分是难点,实在掌握不了可以以后慢慢学。
类型系统其实在很多语言里面都是有的,比如Java、Swift、C++等,但是相对来说TypeScript的类型非常灵活
- 这是因为TypeScript的目的是为 JavaScript 添加一套类型校验系统 因为JavaScript 本身的灵活性,也让TypeScript类型系统不得不增加更多附加的功能以适配JavaScript的灵活性
- 所以TypeScript 是一种可以支持类型编程的类型系统
条件类型
很多时候,日常开发中我们需要基于输入的值来决定输出的值,同样我们 也需要基于输入的值的类型来决定输出的值的类型
条件类型( Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系。
条件类型的写法有点类似于 JavaScript中的条件表达式( condition ? trueExpression : falseExpression)
SomeType extends OtherType ? TrueType : FalseType
type IDType = number | string
// 判断number是否是extends IDType
// 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 {}
条件类型的类型推断infer
在条件类型中推断(Inferring Within Conditional Types) 条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在true分支里引用该推断结果。
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)
type toArray<Type> = Type extends any ? Type[] : never
// string[] | number[]
type newType = toArray<string | number>
如果我们向 ToArray 传入一个联合类型,这个条件类型会被应用到联合类型的每个成员:
- 当传入string | number 时,会遍历联合类型中的每一个成员
- 相当于
ToArray<string> | ToArray<number> - 所以最后的结果是: string[] | number[]
常见的内置类型
一下所举的例子都是内置类型的自我实现,也就是类型体操。
Partial<Type>
用于构造一个 Type下面的所有属性都设置为可选类型
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
type MYPartial<T> = {
[P in keyof T]?: T[P]
}
// IKun都变成可选的
type IKunOptional = MYPartial<IKun>
export {}
Required<Type>
用于构造一个 Type 下面的所有属性全都设置为必填的类型,这个工具类型跟 Partial 相反。
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
type MYRequired<T> = {
[P in keyof T]-?: T[P]
}
// IKun都变成可选的
type IKun2 = MYRequired<IKun>
export {}
Readonly<Type>
用于构造一个 Type下面的所有属性全都设置为只读类型,意味着这个类型的所有的属性都不可以重新赋值。
interface IKun {
name: string
age: number
slogan?: string
}
// 类型体操
type MYReadonly<T> = {
readonly [P in keyof T]: T[P]
}
// IKun都变成可选的
type IKun2 = MYReadonly<IKun>
export {}
Record<Keys, Type>
用于构造一个对象类型,它所有的 key( 键)都是 Keys类型,它所有的value(值)都是Type类型
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 MYRecord<Keys extends keyof any, T> = {
[P in Keys]: T
}
// IKun都变成可选的
type t1 = "上海" | "北京" | "洛杉矶"
type IKuns = MYRecord<t1, IKun>
const ikuns: IKuns = {
"上海": {
name: "xxx",
age: 10
},
"北京": {
name: "yyy",
age: 5
},
"洛杉矶": {
name: "zzz",
age: 3
}
}
export {}
Pick<Type, Keys>
用于构造一个类型,它是从 Type 类型里面挑了一些属性Keys
interface IKun {
name: string
age: number
slogan?: string
}
// 确实keys一定是可以作为key的联合类型
type MYPick<T, K extends keyof T> = {
[P in K]: T[P]
}
// IKun都变成可选的
type IKuns = MYPick<IKun, "slogan"|"name">
export {}
Omit<Type, Keys>
用于构造一个类型,它是从Type类型里面过滤了一些属性Keys
interface IKun {
name: string
age: number
slogan?: string
}
// 确实keys一定是可以作为key的联合类型
type MYOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never: P]: T[P]
}
// IKun都变成可选的
type IKuns = MYOmit<IKun, "slogan"|"name">
export {}
Exclude< UnionType, ExcludedMembers>
用于构造一个类型,它是从 UnionType 联合类型里面排除了所有可以赋给 ExcludedMembers的类型。
type IKun = "sing" | "dance" | "rap"
// 确实keys一定是可以作为key的联合类型
type MYExclude<T, E> = T extends E? never: T
// IKun都变成可选的
type IKuns = MYExclude<IKun, "rap">
export {}
有了 MYExclude,我们可以使用它来实现MYOmit
Extract< Type, Union>
用于构造一个类型,它是从 Type类型里面提取了所有可以赋给 Union 的类型。
type IKun = "sing" | "dance" | "rap"
// 确实keys一定是可以作为key的联合类型
type MYExtract<T, E> = T extends E? T: never
// IKun都变成可选的
type IKuns = MYExtract<IKun, "rap"|"dance">
export {}
NonNullable< Type >
用于构造一个类型,这个类型从 Type中排除了所有的null、undefined的类型。
type IKun = "sing" | "dance" | "rap" | null | undefined
// 确实keys一定是可以作为key的联合类型
type MYNonNullable<T> = T extends null|undefined ? never: T
// IKun都变成可选的
type IKuns = MYNonNullable<IKun>
export {}
ReturnType<Type>
用于构造一个含有 Type 函数的返回值的类型。
// Construct a type consisting of the return type of function type
// 第一个extends是对传入条件进行限制
// 第二个extends是为了进行条件获取类型
type MYReturnType<T extends (...args: any) => any >
= T extends (...args: any) => infer R ? R : never
type T1 = MYReturnType<() => string>
type T2 = MYReturnType<() => void>
type T3 = MYReturnType<(num1: number, num2: number) => string>
function sum(num1: number, num2: number) {
return num1 + num2
}
function getInfo(info: { name: string, age: number }) {
return info.name + info.age
}
type T4 = MYReturnType<typeof sum>
type T5 = MYReturnType<typeof getInfo>
InstanceType< Type>
用于构造一个由所有Type的构造函数的实例类型组成的类型
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)
const d = factory(Dog)
export {}