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
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名