四、TypeScript泛型编程

99 阅读6分钟

一、泛型语法的基本使用

1. 认识泛型

  • 软件工程的主要目的是构建不仅仅明确和一致的API,还要让代码具有很强的可重用性
    • 如可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作
    • 但是对于参数的类型是否也可以参数化呢?
  • 什么事类型的参数化
    • 封装一个函数,传入一个参数,并且返回这个参数
  • 使用TypeScript,考虑这个参数和返回值的类型需要一致,如下的写法不适用于其它类型,如string、boolean、Person等
function func(arg: number): number {
    return arg
}

2. 泛型使用

  • 虽然any是可以的,但是定义为any的时候,其实已经丢失了类型信息
    • 如传入一个number,希望返回的不是any类型,而是number类型
    • 所以,需要在函数中可以捕获到参数的类型是number,并且同时使用它作为返回值的类型
  • 需要使用类型变量(type variable),它作用于类型,而不是值
function func<T>(arg: T): T {
    return arg
}
  • 这里可以使用两种方式来调用它
    • 通过 <> 的方式将类型传递给函数
    • 通过类型推导(type argument inference),自动推导出我们传入变量的类型
      • 这里会推导出它们是字面量类型的,因为字面量类型对于我们的函数也是适用的
func<string>('aaa')
func<number>(111)

func('bbb')
func(222)

3. useState封装

function useState<T>(initialState: T): [T, (newState: T) => void] {
    let state = initialState
    function setState(newState: T) {
        state = newState
    }
    return [state, setState]
}

const [count, setCount] = useState(1)
const [msg, setMsg] = useState('Hello World')
const [banners, setBanners] = useState<any[]>([])

4. 泛型的基本补充

  • 也可以传入多个类型
function func<T, E>(x: T, y: E) {}
  • 平时在开发中可能会遇到的常用名称
    • T:Type的缩写,类型
    • K、V:key和value的缩写,键值对
    • E:Element的缩写,元素
    • O:Object的缩写,对象

二、泛型接口、类的使用

1. 泛型接口

  • 定义接口的时候可以使用泛型
interface IFunc<T> {
    initialValue: T,
    valueList: T[],
    handleValue: (value: T) => void
}
const func: IFunc<number> = {
    initialValue: 0,
    valueList: [0, 1, 3],
    handleValue: function(value: number) {
        console.log(value)
    }
}
  • 给泛型默认值
interface IFunc<T = number> {
    initialValue: T,
    valueList: T[],
    handleValue: (value: T) => void
}

2. 泛型类

  • 编写泛型类
class Point<T> {
    x: T
    y: T
    constructor(x: T, y: T) {
        this.x = x
        this.y = y
    }
}

const p1 = new Point(1, 2)
const p2 = new Point<number>(2, 3)
const p3: Point<number> = new Point(3, 4)

三、泛型约束和类型条件

1. 泛型约束(Generic Constraints)

  • 有时候希望传入的类型有某些共性,但这些共性不在同一个类型中
    • 如 string 和 array 都是有 length的,或某些对象也可以有 length 属性
    • 那么只要是拥有 length 的属性都可以作为参数类型
interface ILength {
    length: number
}

function getLength<T extends ILength>(args: T) {
    return args.length
}

console.log(getLength("1"))
console.log(getLength(["1", "2"]))
console.log(getLength({length: 1, age: 17}))
  • 这里表示传入的类型必须有这个属性,也可以有其他属性,必须有这个成员

2. 泛型约束使用

  • 在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)
    • 可以声明一个类型参数,这个类型参数被其它类型参数约束
  • 希望获取一个对象给定属性名的值
    • 需要确保不会获取 obj 上不存在的属性
    • 在两个类型之间建立一个约束
      • extends
      • keyof 类型 => key 的联合类型
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}
const obj = {
  name: 'ikun',
  age: 25,
}
interface Obj {
  name: string
  age: number
}

const name = getProperty<Obj>(obj, 'xx')  // 类型 ""xx"" 的参数不能赋值给类型 "name" | "age" 的参数
const name = getProperty<Obj, 'xx'>(obj, 'xx') // 类型 ""xx"" 不满足约束 "keyof Obj"

四、TypeScript映射类型

1. 映射类型(Mapped Types)

  • 一个类型需要基于另外一个类型,拷贝一份又不优雅,此时可以考虑映射类型
    • 大部分内置的工具都是通过映射类型来实现的
    • 大多数类型体操的题目也是通过映射类型完成的
  • 映射类型建立在索引签名的语法上
    • 映射类型,就是使用了 PropertyKeys 联合类型的泛型
    • 其中, PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型
interface IPerson {
  name: string
  age: number
}

type MapType<T> = {
  [k in keyof T]: T[k]
}

type NewPerson = MapType<IPerson>

2. 映射修饰符(Mapping Modifiers)

  • 在使用映射类型时,有两个额外的修饰符可能会用到
    • readonly,用于设置属性只读
    • ?,用于设置属性可选
  • 也可以通过前缀 - 或+ 删除或者添加这些修饰符,如果没写前缀,相当于使用了 + 前缀
type MapType<T> = {
  -readonly [k in keyof T]-?: T[k] // 删去 T 中存在所有的 只读属性和 可选属性,改为可写必传
}
interface IPerson {
  readonly name: string
  age?: number
  height: number
}
type NewPerson = MapType<IPerson>
// type NewPerson = {
//   name: string
//   age: number
//   height: number
// }

五、TypeScript条件类型

1. 条件类型(Conditional Types)

  • 开发中需要基于输入的值来决定输出的值,同样也需要基于输入的值类型来决定输出的值的类型
  • 条件类型(Conditional types)用来描述输入类型和输出类型之间的关系
    • 条件类型的写法类似 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression)
      • SomeType extends otherType ? TrueType : FalseType
function sum<T extends number | string>(arg1: T, arg2: T): T extends string ? string : number
function sum(arg1: any, arg2: any) {
  return arg1 + arg2
}
const res1 = sum(1, 2)
const res2 = sum('a', 'b')
const res3 = sum('a', 1) // 类型 1 的参数不能赋值给类型 “"a"” 的参数

2. 在条件类型中推断(infer)

  • 在条件类型中推断
    • 条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果
  • 现有一个函数类型,想要获取到一个函数的参数类型和返回值类型
type CalcFnType = (num: number, str: string) => number

type GetReturn<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never
type GetReturnType = GetReturn<CalcFnType> // type GetReturnType = number

type GetParams<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never
type GetParamsType = GetParams<CalcFnType> // type GetParamsType = [num: number, str: string]

3. 分发条件类型(Distributive Conditional Types)

  • 在泛型中使用条件类型的时候,如果传入一个联合类型,会变成分发的(distributive)
type ToArray<T> = T extends any ? T[] : never
// type newType = string[] | number[]
type ToArray<T> = T[]
// type newType = (string | number)[]
type newType = ToArray<string | number>
  • 在 ToArray 传入一个联合类型,这个条件类型会被应用到联合类型的每个成员
    • 当传入 string | number 时,会遍历联合类型中的每一个成员
    • 相当于 ToArray<string> | ToArray<number>
    • 最后的结果是:string[] | number []

六、类型工具和类型体操

1. 内置工具和类型体操介绍

  • 类型系统其实在很多语言中都是有的,如Java、Swift、C++等,但相对来说 ts 的类型更灵活
    • 这是因为 typescript 的目的是为 JavaScript 添加一套类型校验系统,因为 JavaScript 本身的灵活性,让 typescript 类型系统不得不增加更附加的功能以适配 JavaScript 的灵活性
    • 所以 typescript 是一种可以支持类型编程的类型系统
  • 这种类型编程系统为 typescript 增加了很大的灵活度,同时也增加了它的难度
    • 业务开发基本不需要太多的类型编程能力
    • 开发框架、库、或通用性工具,为了考虑各种适配情况,就需要使用类型编程
  • typescript 本身提供了类型工具,可以辅助进行类型转换(之前有关于this的类型工具)
  • 类型体操题目