得泛型者得 TS

170 阅读8分钟

(一)泛型介绍

1.需求

创建一个 fn 函数,它的特点是: 传入什么类型(可能有多种类型)的数据就返回什么类型的数据。 也就是说,参数和返回值类型相同。

{
    function fn(a: number): number { return a }
    fn(10)
}

该函数声明只接收数值类型,无法用于其他类型

2.思考:any 大法能做到么?

{
    function fn(a: any): any { return a }
    fn(10)
    fn('啦啦啦')
    fn([1, 3, 'Lucy', false])
}

不能。它没有了类型保护,也没有严格限制返回值的类型和输入值的类型一致。

3.引入泛型

泛型,顾名思义,就是可以适用于多个类型,使用类型变量(比如T)帮助我们捕获传入的类型,之后我们就可以继续使用这个类型。

本质是参数化类型,通俗的讲,就是所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和函数的创建中,分别成为泛型类泛型接口泛型函数

(二)泛型函数

1.泛型函数:

这个函数的参数类型或者返回值的类型是可变的

2.定义格式:

(1)语法:

function 函数名<类型变量1,类型变量2,...>(参数1:类型1,参数2:类型2,...): 返回值类型 {}

  1. 在函数名称的后面写 <>(尖括号),尖括号中添加类型变量
  2. 类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)

(2)示例:

{
    // 这里 Type 只是一个 类型名称,改成其他名字也可以
    function fn<Type>(value: Type): Type { return value }
}
  1. 类型变量 Type,是一种特殊类型的变量,它处理类型而不是值
  2. 因为 Type 是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型
  3. 类型变量 Type,可以是任意合法的变量名称

3.调用格式

(1)语法:

const 返回值 = 泛型函数名<类型1,类型2,...>(实参1,实参2,实参3,......)

  • 在函数名称的后面添加 <>(尖括号),尖括号中指定具体的类型

(2)示例:

{
    // 定义函数
    function fn<Type>(value: Type): Type { return value }
    // 调用函数
    const age = fn<number>(18)
    const str = fn<string>('a')
}

小结:

  1. 当传入类型 number 后,这个类型就会传递给函数声明时对应的类型变量
  2. 通过泛型就做到了让 fn 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全

(三)类型推断简化函数调用

1.问题

调用函数时,需要显示设置类型参数,太麻烦啦!

{
    function fn<Type>(value: Type): Type { return value }
    // 调用函数,需要设置类型参数
    const num = fn<number>(10)
}

2.类型推断简化函数调用

{
    function fn<Type>(value: Type): Type { return value }
    const num = fn(10)
    const str = fn('lalala')
}

小结:

  1. 在调用泛型函数时,可以省略 <类型> 来简化泛型函数的调用
  2. 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量 Type 的类型
  3. 比如,传入实参 10,TS 会自动推断出变量 num 的类型 number,并作为 Type 的类型

推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读

(四)模拟实现 useState 的声明

{
    // ◆ 改造 useState 使用元组
    function useState<T>(value: T): [T, (newValue: T) => void] {
        const setValue = (newValue: T) => {
        }
        return [value, setValue]
    }

    // ◆ 可以省略 <类型> 来简化泛型函数的调用
    const [str, setStr] = useState('123')
    // const [str, setStr] = useState<string>('123')

    const [num, setNum] = useState(123)
    // const [num, setNum] = useState<number>(123)
}

(五)泛型约束

1.泛型的问题

默认情况下,泛型函数的类型变量 T 可以代表多个类型,这导致在泛型函数内部无法访问任何属性

{
    function fn<T>(value: T): T {
        // 这里value. 不会有提示
        console.log(value.length)// 这里会报错
        return value
      }
      fn('abc')
}

image.png

  • T 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length
  • 此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围),从而得到类型提示

2.添加泛型约束

添加泛型约束收缩类型,主要有两种方式:指定更加具体的类型添加约束

(1)指定更加具体的类型

比如,从任意类型的数据 -----> 任意类型的数组。

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

{
    // function fn<T>(value: Array<T>): Array<T> {
    function fn<T>(value: T[]): T[] {
        // 这里就可以获得提示 value.
        console.log(value.length)
        return value
    }
    fn([1, 2, 3])
}

image.png

(2)添加约束

比如,要求传入 T 类型必须要有 length 属性。

思路:对已有类型做继承( interface 接口的继承)

// ◆ 示例1:
{
    // 1.创建一个 interface 接口
    interface ILength { length: number }
    // 2.类型参数做继承接口
    function fn<T extends ILength>(value: T): T {
        console.log(value.length);// 这里有 length 提示
        return value
    }
    fn<string>('abc')
    fn<number[]>([6, 8, 9])
    fn([1, 2, 3])
}
// ◆ 示例2:
{
    function setElement<Type extends HTMLElement>(element: Type): Type {
        console.log(element.innerText) // 这里的 element.会有语法提示
        return element
    }
}

小结:

  1. 创建描述约束的接口 ILength,该接口要求提供 length 属性
  2. 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束
  3. T extends ILength约束表示:传入的类型必须具有 length 属性

注意:传入的实参(比如,数组)只要有 length 属性即可(类型兼容性)

(六)泛型经典应用

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

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

{
    // ◆泛型的经典应用
    function getProp<Type, K extends keyof Type>(obj: Type, key: K) {
        return obj[key]
    }
    let person = { name: 'jack', age: 18 }
    getProp(person, 'age')// 这里有 类型提示了
}

小结:

  1. 添加了第二个类型变量 Key,两个类型变量之间使用 , 逗号分隔。
  2. keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型
  3. 本示例中 keyof Type 实际上获取的是 person 对象所有键的联合类型,也就是:'name' | 'age'
  4. 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性

image.png

(七)泛型接口

在接口中使用泛型来使用,以增加其灵活性,增强其复用性

(1)格式:

interface 接口名<类型变量1,类型变量2...> {
  属性名1:类型1,
  属性名2:类型2,
  属性名3:类型3
}

(2)示例:

{
    // ◆ 模拟数组,使之拥有数组的某些方法
    // interface MyArray {
    //     length: number,
    //     push(n: number):void,
    //     pop():number,
    //     reverse():number[]
    // }

    interface MyArray<T> {
        length: number,
        push(n: T): void,
        pop(): T,
        reverse(): T[]
    }

    let arr1: MyArray<number>
    // arr1.push()
    let arr2: MyArray<string>
    // arr2.push()
}

image.png

(3)小结

  1. 在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口。
  2. 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量
  3. 使用泛型接口时,需要显示指定具体的类型

(4)拓展

实际上,JS 中的数组在 TS 中就是一个泛型接口。泛型接口无处不在呀!

第一步:在 js 文件中随意书写一个数组,push一下

const arr=[1,2,3,4]
arr.push(5)

第二步:对准 push 按住 Ctrl + 鼠标左键,戳进去有惊喜

push(...items: T[]): number;
// 除了 push 还可以看到数组的所有泛型接口
pop(): T | undefined;
join(separator?: string): string;
reverse(): T[];
shift(): T | undefined;
slice(start?: number, end?: number): T[];
sort(compareFn?: (a: T, b: T) => number): this;
splice(start: number, deleteCount?: number): T[];
splice(start: number, deleteCount: number, ...items: T[]): T[];
unshift(...items: T[]): number;
// ......

image.png

小结

  • 当我们在使用数组时,TS 会根据数组的不同类型,来自动将类型变量设置为相应的类型
  • 可以通过 Ctrl + 鼠标左键(Mac:Command + 鼠标左键)来查看具体的类型信息

(八)泛型工具类型

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

说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。 这些工具类型有很多,我们来看看常见的几个。

1.Partial

Partial<Type> 用来基于某个 Type 来创建一个新类型,新类型中所有的属性是可选的。

(1)格式

type OldType = { 属性1:类型1,....}
type NewType = Partial<OldType>

(2)示例

{
    type ProsType = {
        id: number
        name: string
        hobby: string[]
        // hobby:Array<string>
    }
    // ◆ 可选,部分 Partial<Type>
    let p1: Partial<ProsType> = { name: 'Lucy' }
    let p2: Partial<ProsType> = { id: 7 }
    let p3: Partial<ProsType> = { hobby: ['玩魔方'] }
}

2.Readonly

Readonly<Type> 用来构造一个类型,将 Type 的所有属性都设置为 readonly (只读)--不可修改。

(1)格式

type OldType = { 属性1:类型1,....}
type NewType = Readonly<OldType>

(2)示例

{
    type ProsType = {
        id: number
        name: string
        hobby: string[]
        // hobby:Array<string>
    }
    // ◆ 只读  Readonly<Type>
    let p1: Readonly<ProsType> = { id: 1, name: 'Jan', hobby: ['敲代码'] }
    // p1.name='Jane'  会报错,因为只读,不能修改
}

3.Pick

Pick<Type, Keys>从已有的类型中挑选一组属性,来构造新类型。

(1)格式

type OldType = { 属性1:类型1,....}
type NewType = Pick<OldType, Keys>

(2)示例

{
    type ProsType = {
        id: number
        name: string
        hobby: string[]
        // hobby:Array<string>
    }
    // ◆ 挑选 Pick<Type, Keys> 
    let p1: Pick<ProsType, 'id'> = { id: 0 }
    let p2: Pick<ProsType, 'id' | 'name'> = { id: 3, name: 'Jack' }
}