TypeScript 泛型编程:入门

88 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 3 天

泛型就像函数

const f = (a, b) => a + b
const result = (1, 2)  // 3

如果你能看懂 JS 的函数,那么你就能看懂 TS 的泛型

type F<A, B> = A | B
type Result = F<string, number>  // string | number

它们的唯一区别就是 () 变成了 <>, const 变成了 type

我们可以认为函数是接受其它代码的代码。

我们可以认为泛型是接受其它类型的类型。

Screen Shot 2022-10-11 at 9.15.48 PM.png

所以 JS 的函数和 TS 的函数,只是操作的对象不一样,JS 操作值,TS 操作类型。

函数的本质是什么?

推后执行的、部分待定的代码。

// 当我们写如下代码的时候,这个代码在我们写的时候
cosole.log(1)  // 只要执行到这一行,是不是就运行了

// 如果我们咬他延后执行,该怎么办?
const f = () => console.log(1)  // 如果把它声明成一个函数,它的定义和它的执行就分离了,我只定义,并没有执行。

// 推迟执行
setTimeout( () => {
 f()
}, 3000 )  // 3s 后执行这个函数

所以函数的本质在面向过程式编程里面,它就是把这个执行时机往后推。

它还可以把代码的一块扣出来等到你未来去填这一块

const f = () => console.log(1)  // 你永远打印 1

const f2 = x => console.log(x)  // 把里面的 值 扣出来 用 变量 x,这里的 x 就是部分待定的代码

// 等你想执行的时候你可以把你扣的那一块,在用你想传的值 给填上去
f2(2222) 

const f3 = (fn) => fn(1)

// 这里使用 console.log 只是为了举例,实际使用可能有问题
f3(console.log) // 这样的话等 执行 f3 的时候,是不是相当于执行 console.log(1)

const f4 = (fn, n) = fn(n)

f4(console.log, 222222)  
// 其实函数的本质是我现在有些东西决定不了,或者我希望是别人决定的。

那么泛型的本质就是推后执行的、部分待定的类型。

为什么会有泛型

function echo(whatever: number | string | boolean) {
  return whatever
}

// 这个 echo 函数的类型是什么?

我们所认为的

参数类型     number | string | boolean
返回值类型   number | string | boolean

以上这种形式不对,因为这种形式隐含了一些错乱的情况,比如说我是不是可以接受一个 boolean 返回一个 number,我是不是可以接受一个 number 返回一个 boolean。

但是我们想要的情况是一一对应的,接受什么就返回什么。

参数类型     number  string  boolean
返回值类型   number  string  boolean

我们需要对你的类型进行精细化的管理,而非笼统的。

那我做类型收窄不行吗?

// 就算这样写,依然会得到错误的答案
function echo(whatever: number | string | boolean) {
  switch (typeof whatever) {
    case 'number':
      return whatever
      break;
    case 'string':
      return whatever
      break;
    case 'boolean':
      return whatever
      break;
  }
}

依然存在错乱的情况 Screen Shot 2022-10-11 at 10.25.24 PM.png

通过以上例子得出:

没有泛型,有些奇怪的需求就无法满足,没有泛型的类型系统,就如同没有函数的编程语言。

简单的泛型

代入法

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
}

interface Hash<V> {
  [key: string]: V
}
interface List<A> {
  [index: number]: A
}

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

这个的意思就是

// 使用代入法
interface X {
  [index: number]: string
}

interface Y {
  [index: number]: string | number
}

默认类型

如果不传就会报错 Screen Shot 2022-10-11 at 10.47.12 PM.png

那能不能给参数一个默认值呢?

// JS 函数参数给默认值
function a(b=1) {return b}
// 触类旁通
// <A = string>  类型参数的默认值
interface List<A = string> {
  [index: number]: A
}

// 以下都可以,没有报错
type X = List

// 可传可不传
type X1 = List<string>
type X2 = List<string | number>

interface Person {
  name: string
}

type X3 = List<Person>

在泛型中使用 extends

extends 不要读作继承,因为继承只对面向对象有意思,可以读作 包含于

type  Person = {name: string}

// = 右边的表达式 叫做 条件类型 Conditional Types
type LikeString<T> = T extends string ? true : false
type LikeNumber<T> = T extends number ? 1 : 2
type LikePerson<T> = T extends Person ? 'yes' : 'no'

type R1 = LikeString<'hi'>  // true 
type R2 = LikeNumber<true>  // false
type S1 = LikeNumber<6666>  // 1
type S2 = LikeNumber<false>  // 2
type T1 = LikePerson<{ name: 'hone', xxx: 1 }>  // yes
type T2 = LikePerson<{ xxx: 1 }>  // no

Screen Shot 2022-10-12 at 12.01.52 PM.png

// unknown 是包含所有类型的, 所以这个 T 必然 包含于 unknown
type ToArray<T> = T extends unknown ? T[] : never
type Result = ToArray<string | number>
// 代入过程
// type Result = (string | number) extends unknown ?...
// type Result = (string extends unknown ?...)
//               | (number extends unknown ?...)
// type Result = string[] | number[]

Screen Shot 2022-10-12 at 2.47.57 PM.png

为何是上面的这种情况?

我们在使用泛型的时候,很有可能是在函数里面使用的

type ToArray<T> = T extends unknown ? T[] : never
// 伪代码
function f(a: T): ToArray<T>{
  if(typeof a === 'string') {
    return string[]
  } else if(typeof a === 'number') {
    return number[]
  }
} 

所以根据我们写代码的习惯来看,这个 TS 的预判是对的。它确实应该分开操作,它其实在模拟 JS 函数的思路先把泛型分别收窄分别做计算。

type X1 = LikeString<never> 如果传的是 never, 把它理解成并集,就会发现它是空的,没有东西并上没有东西,那如果把它分散开,并集是不用分散的,所以就直接返回一个 never。

type Result = ToArray<string | number> 之所以要分开,是因为它有两个选项,可以分两个分支,type X1 = LikeString<never> 它只有一个 never,没有分支,结果就必然是一个 never。

type ToArray<T> = T extends unknown ? T[] : never
type Result = ToArray<never>
// type Result = never extends unknown ? ...
// type Result = 空集没有元素直接返回 never
// type Result = never

Screen Shot 2022-10-12 at 7.38.53 PM.png

规则:

  • 若 T 为 never,则表达式的值为 never
  • 若 T 为 联合类型,则分开计算

如果你用的是泛型而这个泛型又正好跟联合类型用到一起了,它就很像乘法:

乘法的分配律(A + B) * C = A * C + B * C

泛型之于联合类型的分配律(string | number) extends ? 这里就不能直接 extends, 因为 string 和 number 没有交集,必须先分配。

乘法中的: 0 * C = 0

遇到 never 永远是 never

注意:只对泛型有效

Screen Shot 2022-10-12 at 7.56.41 PM.png 为何会出现以上情况?因为没有用函数

// 发现如果不是泛型 无法写
// 只能用自己骗自己的写法
// 那么这里的值就没有办法像泛型那样灵活
type X2 = string | number extends unknown ? (string | number)[] : never

如果你不是泛型,你就根据最基础的知识,看它的集合是否包含于另外一个集合之中。

在泛型中使用 keyof

// 获取 Person 所有的 key
type Person = { name: string, age: number }
type GetKeys<T> = keyof T
type Result = GetKeys<Person>
//   ^-- 'name' | 'age'

在泛型中使用 extends keyof

type Person = {name: string, age: number}
// K extends keyof T 泛型约束 我要约束这个 K
// 这里的 T[K] 就是 Person['name']  Person['age']
type GetKeyType<T, K extends keyof T> = T[K] 

type Result1 = GetKeyType<Person, 'name'>
//   ^-- string
type Result2 = GetKeyType<Person, 'age'>
//   ^-- number```