TS 泛型的使用

大纲链接 §

[toc]


简单的泛型 Generic Types

一般类型(非泛型)的局限 V.S. 泛型的作用

有一个直接返回参数的函数

1
2
3
function returnIt(sth: unknown): unknown {
  return sth
}
  • 以上的函数不能表示 参数类型需要与返回值类型相同的情况

使用泛型

1
2
3
4
5
6
7
function returnIt<T>(sth: T): T {
  return sth
}

let s = returnIt<string>('hi')
// s = returnIt<number>(1)
// s = returnIt<boolean>(true)
  • 泛型 可以理解为 不定类型,用来表示多种类型
  • 将泛型的类型 string 代入就可得到 s 的类型
  • 一旦确定类型,将不同类型重复赋值相同变量名,也会类型报错
  • 可以使用任意大写字母表示,一般使用尖括号来声明 <T>,在冒号后像类型一样使用

返回数组类型的例子

1
2
3
4
5
6
7
8
9
function returnArray<T>(array: T[]): T[] {
  return array
}
let a = returnArray([])

let ss: Array<string> = ['a', 'b']

type H = {name: string}
let aa: Array<H> = returnArray<H>([{ name: 'a' }, { name: 'b' }])

不推荐的写法

1
2
3
4
5
6
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
let myIdentity2: {<T>(arg: T): T} = identity;
// error // let myIdentity2: <T>(arg: T): T = identity;
  • <T>(arg: T) => T 和 函数中的类型声明 <T>(arg: T): T 等价

区别于类型断言

类型断言,在编译时起作用

  • “尖括号” 用法

    1
    2
    
    let someValue: unknown =  ''
    let strLength: number = (<string>someValue).length
    
  • as 语法

    1
    2
    
    let someValue: unknown =  ''
    let strLength: number = (someValue as string).length
    

泛型常用组合形式

  • 泛型 + 接口
    • interface x<T> {(a: T): T;}
    • 使用 let xxx: x<string> = (a) => a;
  • 泛型 + 类型别名 type t<T> = {a: T;}
  • 多个泛型 type t<T, P> = {a: T; b: P}
  • 泛型 + 函数
    • function fn<T>(arg: T): T {}
  • 泛型 + 类
    • class C<T> {attr: T; fn: (a: T) => T }
    • 使用 let cc = new C<string>()
  • 约束 构造函数/类 new(): T

    • JS 工厂函数 function create(C) {return new C()}
    • 使用泛型 function create<T>(C: {new (): T): T { return new C()}
    • 表示 实例对象的类型 为 T;传递的参数为一个 类 C

      1
      2
      3
      4
      5
      6
      7
      8
      
      function create<T>(C: {new(): T}) {
      return new C()
      }
      class Human {}
      class Animal {}
      let P = create<Human>(Human)
      let P1 = create<Human>(Animal) // 报错
      let S = create<String>(String)
      

泛型就像函数

1
2
3
4
5
6
const f = (a, b) => a + b
const result = f(1, 2)

type F<A, B> = A | B
// 使用泛型得到一个新的更具体的类型
type Result = F<string, number>

语法

类比函数

区别项 \ 类比项 名称 参数列表
可传默认项
主体 调用 返回项
函数 函数名 形参/实参
(默认值)
函数体 函数调用 返回值
泛型 具名:类型别名/接口等
匿名:函数/类等
泛型参数 / 代入具体类型
(默认类型)
类型主体(包含属性类型声明、类型集合、集合运算等逻辑) 类型调用 返回新的类型

泛型是面向对象的经典概念之一

  • 泛型,即“参数化类型”,也是一种类型
  • 泛型是一种接受其他类型的类型
  • 也称为泛型函数

    1
    2
    3
    4
    5
    6
    7
    
    type A = 'hi' | 123 // TS
    let a = ['hi', 123]
    
    type F<T> = T | T[]
    // type F_number = F<number>
    // type F_string = F<string>
    const fn = (x: number) => x + 1
    
  • 泛型:未被声明具体类型的类型(类似 unknown),type X<T> = T extends unknown ? T : never

  • 可以理解为类型的占位符

  • 函数的本质是:

    • 延后执行 一段逻辑,之前学过函数调用的时机,声明和执行分离
    • 部分待定的代码作为参数,调用时才填入具体值
  • 类比泛型

    • 延后声明 具体类型,先用占位符表示,之后使用到时再替换为具体类型
    • 部分待定的类型作为参数,调用时才填入具体类型

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      const add = (a: number, b: number) => a + b
      // const add = (a: string, b: string) => a + b
      
      type Add<T> = (a: T, b: T) => T
      // type AddNumber = Add<number>
      // type AddString = Add<string>
      
      const addN: Add<number> = (a, b) => a + b
      const addS: Add<string> = (a, b) => `${a} ${b}`
      
      function returnIt<T>(arg: T): T{
      return arg;
      }
      
      let s = returnIt<string>('hi')
      
  • 具体代码:码上掘金,鼠标滑过类型查看类型提示

  • 以函数形参来类比泛型,实参就可以理解为:为泛型明确声明具体类型,可称为泛型的类型收窄

为什么会有泛型

  • 弱类型语言不需要泛型,比如JS
  • 具有复杂的类型系统的计算机语言需要泛型,比如JAVA

    1
    2
    3
    
    function echo(whatever: string | number | boolean) {
    console.log(typeof whatever)
    }
    
  • 判断分支代码无法实现以一对应,存在类型不精确或错乱的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // bad
    function echo(whatever: string | number | boolean) {
    switch(typeof whatever)
    case('string')
    case('number')
    case('boolean')
    default return whatever
    // ...
    }
    
  • 如果参数的类型和返回的类型有特殊的要求,比如一一对应,就需要泛型

  • 判断分支代码无法实现以一一对应,存在类型不精确或错乱的情况

  • 没有泛型,就无法实现复杂的逻辑需求

  • 没有泛型的类型系统,就如同没有函数的编程语言


一星难度的 TS 体操:代入法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type Union<A, B>= A | B
type Union3<A, B, C>= A | B | C

type Intersect<A, B> = A & B
type Intersect3<A, B, C> = A & B & C

// 列表值类型
interface List<A> {
  [index: number]: A
}

type L = List<string>
type Y = List<string | number>

// 代入法
/*
interface L = {
   [index: number]: string   
}
interface Y = {
   [index: number]: string | number
}
*/

// 哈希值类型
interface Hash<V> {
  [key: string]: V
}
type S = Hash<string>
type N = Hash<number>
// 泛型可传入复杂类型 如接口
type F = Hash<List>
  • 鼠标悬浮的提示无法知道详细类型
  • 可使用代入法理解
  • 无需逻辑操作

可传类型默认值

1
2
3
interface Hash<V = string> {
  [key: string]: V
}

二星难度的 TS 体操:条件类型

补全类型

  1. 提示:使用条件类型 Conditional Types,即三元表达式
  2. 提示:类型的 关系运算符 只有一种: extends 意为包含于,表示类型集合的从属关系

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    type Person = {name: string}
    
    /*
    type LikeString<T> = ???
    type LikeNumber<T> = ???
    type LikePerson<T> = ???
    */
    
    // 补全上分代码,可实现
    type S1 = LikeString<'hi'> // true
    type S2 = LikeString<true> // false
    type N1 = LikeNumber<8> // 1
    type N2 = LikeNumber<false> // 2
    type P1 = LikePerson<{name: 'frank', xxx: 1}> // yes
    type P1 = LikePerson<{xxx: 1}> // no
    
  • 集合的关系判断无法像值那样,有大于小于或相等的判断,只有从属关系的判断
  • 集合范围决定了是否为从属包含关系,范围完全一样的是一种特殊的情况
  • extends 读作 “包含于”,而不是面向对象中的继承

    1
    2
    3
    
    type LikeString<T> = T extends string ? true : false
    type LikeNumber<T> = T extends number ? 1 : 2
    type LikePerson<T> = T extends Person ? 'yse' : 'no'
    

条件类型的特殊情况 never

  • never 可理解为空集,但在类型判断中,存在特殊情况

    1
    2
    
    type LikeString<T> = T extends string ? true : false
    type X1 = LikeString<never> // X1: never 而不是 true
    

规则

  1. <T> 为联合类型,则各个类型,分开计算
  2. <T> 代入 never,则表达式的值为 never

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    type ToArray<T> = T extends unknown ? T[] : never
    type res = ToArray<string | number>
    // res 结果为
    // (string | number)[] 类型
    // string[] | number[] 类型 √
    /*
    * 使用代入法
    * (string | number) extends unknown ? ...
    * (string extends unknown ? ...)
    *  | (number extends unknown ? ...)
    *  string[] | number[]
    */
    
  • 联合类型的条件判断,分开计算可以理解为 进行类型收窄时的处理过程

    1
    2
    3
    4
    5
    6
    7
    
    type ToArray<T> = T extends unknown ? T[] : never
    type res = ToArray<never>
    /**
    * res 的类型为
    * never[]
    * never √
    */
    
  • 空集没有元素,直接返回 never

使用类比来理解

  • 泛型 + 联合类型,比作乘法的 分配律 (A | B) extends X -> A extends X | B extends X
  • never 比作乘法中的 0
  • 只对泛型有效

在泛型中使用 extends 泛型约束

  • 给泛型添加类型约束,缩小泛型的范围
  • 关键字 extends,翻译为包含于(集合范围小于或等于),区别于类class的继承关键字

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    // 添加约束之前
    function returnIt<T>(arg: T): T {
    console.log(arg.length) // error
    return arg;
    }
    
    // 添加约束之后
    interface HasLength{
    length: number
    }
    
    function returnIt<T extends HasLength>(arg: T): T {
    console.log(arg.length) // no error
    return arg;
    }
    
  • 声明的泛型 <T> 必须满足 包含于 HasLength 接口


在泛型中使用 keyof


在泛型中使用 extends keyof


泛型的实际使用:在React中使用泛型

  • 安装 reactreact-dom
  • 安装 @types/react@types/react-dom
  • Ctrl + 点击 查看泛型是什么,使用代入法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    import React, { FunctionComponent, FC } from 'react'
    
    type P = { name: string }
    
    // 代入法
    /*
    interface FunctionComponent<P = {}> = {
    (props: P, context?: any): ReactElement<any, any> | null;
    // ...
    }
    */
    const App: FunctionComponent<P> = (props) => {
    console.log(props.name)
    console.log(props.children)
    return (
    <div>hi</div>
    )
    }
    

泛型 区别于 函数重载

  • 泛型:使用时声明具体类型类型
    • 难以根据不同条件,约束所有参数的类型
  • 函数重载:根据参数类型条件判断

    • 支持多种属性签名

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      // 泛型
      type Add<T> = (a: T, b: T) => T
      const add: Add<string | number> = (a, b) => { // 防止出现 a: string, b: number 的情况,必须使用 函数重载
      return a + b
      }
      
      // 函数重载
      function add2 (a: string, b: string): string; // 重载1
      function add2 (a: number, b: number): number; // 重载2
      function add2 (a: unknown, b: unknown): unknown { // 具体实现
      if(typeof a === 'string' && b === 'string' ) return `${a} ${b}`
      if(typeof a === 'number' && b === 'number' ) return a + b
      }
      
      add2(1, 2)
      add2('1', '2')
      // add2('1', 2) // error
      
  • 让函数满足多种的参数条件就是函数重载


如何用泛型封装网络请求库

  • 使用 axios 时 可以根据参数的类型,来自动判断执行的逻辑分支
    • axios.get('/url', {})
    • axios.get({url: '/xxx', header: ''})
    • axios.get(url?: string, options?: {url: string, header: unknown}): void
  • 拿获取用户来举例

    • const user = createResource('/api/v1/user')

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      
      import axios from 'axios'
      type User = {
      id: string | number;
      name: string;
      age: number
      }
      
      type Resonse<T> = {data: T}
      type O = Omit<Partial<User>, 'id'>
      // type O2 = Partial<Omit<User, 'id'>>
      
      type CreateResource = (path: string) => {
      create: (arrts: O) => Promise<Resonse<User>>;
      delete: (id: User['id']) => Promise<Resonse<never>>;
      update: (id: User['id'], attrs: O) => Promise<Resonse<User>>;
      get: (id: User['id']) => Promise<Resonse<User>>;
      getPage: (page: number) => Promise<Resonse<User[]>>;
      }
      
      const createResource: CreateResource = (path) => {
      return {
      create(attrs) {
      const url = `${path}s`
      return axios.post<User>(url, {data: attrs})
      },
      delte(attrs) {},
      update(attrs) {},
      get(attrs) {},
      getPage(attrs) {},
      }
      }
      
      const user = createResource('/api/v1/user')
      

使用映射类型 in


难度为三星的泛型类型体操


如何使用 -readonly


类型体操有多难


如何提问

  • stackblitz.com
    • vanilla TS
    • 需要默认导出任意 export {} 来消除报错
  • 码上掘金
  • 将有疑问的代码在平台上运行,保存后分享路径 url
  • 描述问题

·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名