likes
comments
collection
share

Typescript中对泛型的理解

作者站长头像
站长
· 阅读数 16

泛型的作用

先看下面的代码:

function echo(arg) {
    return arg
}
const result = echo('str')

这个时候 result 是一个 any 类型,any 类型在 ts 中是不推荐的,ts 所有的思想就是获得类型,如果没有获得类型,ts的威力就减少了90%以上。

一般 ts 有个类型推断, 比如: let test = 123, 这个时候 test 类型推断为一个 number类型。

按道理来说,我们传入了一个字符串类型,函数体直接返回了入参,那么返回值也应该是字符串,那为什么是any类型呢?

这是因为,类型推断无法推断函数里面的参数arg,尽管我们传入的参数是一个字符串,但类型推断无法进入到函数里面去推断

但我们仍然还想根据传入参数的不同,返回不同的类型。

所以,泛型就诞生了。

泛型是指在定义函数,接口或类的时候,不先预先指定具体的类型,而使用的时候再指定类型的一种特性

泛型的使用

函数

泛型很多场景是跟函数绑定在一起的,先看看泛型在函数上的使用。

function identity<T>(value: T): T {
    return value
}

let output = identity<string>('mystring')
// 可以省略<string>, ts帮我们做了类型推论,推断T是字符串,推荐这种方式,代码比较精简。
let output1 = identity('string')

接口

泛型应用在接口上有两种写法,主要是泛型的位置不同。首先看第一种:

// 定义一个泛型接口 IPerson表示一个类,它返回的实例对象取决于使用接口时传入的泛型T
interface IPerson<T> {
  new(...args: unknown[]): T;
}

function getInstance<T>(Clazz: IPerson<T>) {
  return new Clazz();
}

class Person {}

// TS推断出函数返回值是person实例类型
const person = getInstance<Person>(Person);

当把泛型、接口、类稍稍融合下,就不太容易看懂了,这也是学习 ts 的难点。下面一行一行的分析下:

  1. 定义了一个类的泛型构造函数接口IPerson<T>
  2. 定义了一个泛型函数getInstance<T>,参数必须是一个类的构造函数Clazz: IPerson<T>
  3. 定义了一个类的构造函数class Person
  4. 执行函数getInstance<Person>(Person),也可以简写:getInstance(Person)

在上面的例子中,使用泛型接口时需要在使用接口时声明该 T 类型,比如IPerson<T>

接下来再看泛型接口另一种写法:

// 声明一个接口IPerson代表函数
interface IPerson {
  // 此时注意泛型是在函数中参数 而非在IPerson接口中
  <T>(a: T): T;
}

// 函数接受泛型
const getPersonValue: IPerson = <T>(a: T): T => {
  return a;
};

// 相当于getPersonValue<number>(2)
getPersonValue(2)

关于泛型接口中泛型的位置是代表完全不同的含义:

  • 当泛型出现在接口中时,比如interface IPerson<T> 代表的是使用接口时需要传入泛型的类型,比如IPerson<T>
  • 当泛型出现在接口内部时,比如第二个例子中的 IPerson接口代表一个函数,接口本身并不具备任何泛型定义。而接口代表的函数则会接受一个泛型定义。换句话说接口本身不需要泛型,而在实现使用接口代表的函数类型时需要声明该函数接受一个泛型参数。

class GenericNumber<T> {
  zeroNumber: T
  constructor(x: T) {
    this.zeroNumber = x
  }
  add(x: T, y: T): T {
    return x
  }
}

let myGeneric = new GenericNumber<number>(100)
myGeneric.zeroNumber = 10
myGeneric.add = (x, y) => {
  return x + y
}

Promise 中的泛型

function withAPI(url: string) {
  return fetch(url).then(res => res.json())
}
withAPI('bai.com').then(res => {}) 

这个时候 res 是 any 类型,这不满足我们的要求,所以可以加上泛型。

interface BaiRes {
  name: string
  count: number
}
// Promise<T> 表示泛型在promise上传递
function withAPI<T>(url: string): Promise<T> {
  return fetch(url).then(res => res.json())
}
withAPI<BaiRes>('bai.com').then(res => {}) // 这个时候res就是BaiRes类型了

withAPI 函数返回一个 Promise,但是 Promise 返回的值没有类型,所以就加上了Promise<T>,这样函数传入的类型 BaiRes,就可以流入到 Promise 当中了,此时就可以看到 res 是一个 BaiRes 类型的对象。

再来看一个例子:

function loadImg(src: string) { 
  const promise = new Promise((resolve, reject) => {
      const img = document.createElement('img')
      img.onload = () => { 
          resolve(img)
      }
      img.onerror = () => { 
          reject('图片加载失败')
      }
      img.src = src
  })
  return promise
}

const result = loadImg('src')
result.then((img: HTMLImageElement) => {
  console.log('img.width', img.width)
  return img
})

此时,img: HTMLImageElement 这里会报错,这是因为 promise 返回的 img 的类型并不一定是 HTMLImageElement 类型。

Typescript中对泛型的理解

所以,需要给 promise 添加上泛型的类型 HTMLImageElement,这样类型 HTMLImageElement 就会流入到返回的值img上。

function loadImg<T>(src: string): Promise<T> { 
  const promise = new Promise<T>((resolve, reject) => {
      const img = document.createElement('img')
      img.onload = () => { 
          resolve(img as T)
      }
      img.onerror = () => { 
          reject('图片加载失败')
      }
      img.src = src
  })
  return promise
}

const result = loadImg<HTMLImageElement>('src')
result.then((img) => {
  console.log('img.width', img.width)
  return img
})

Typescript中对泛型的理解

泛型约束

上面讲了泛型在函数、接口、类及Promise上的使用,希望你能按照例子手动写一遍,加深对泛型的理解,最后来看下泛型约束。

在函数体内,要访问参数的长度,但是并不是所有的参数都有 length 属性,会在编译时 ts 会直接给出错误。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length
    return arg
}

Typescript中对泛型的理解

针对这种情况,就必须要对泛型进行约束,只能传具有length属性的参数,那么就可以使用泛型约束,来约束传入的参数必须有length属性。

interface Lengthwise {
    length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length
    return arg
}

再来看另外一个例子:泛型 K 只能取 obj 里面的属性,不能获取之外的属性。

// `keyof`用来获取获取泛型 T 的 key
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key]
}
let x = {a1b2c3}
getProperty(x, 'a') // ok
getProperty(x, 'm'// 报错

总结

本文把泛型的概念及基本使用都梳理了一遍,但是这远远不够,当泛型和类、接口、函数相结合时可以衍生出很多类型,这就是 typescript 的类型编程。对于初学者来说,这也是最大的挑战,但是从另一个角度来看,一门技术提高了门槛,就会屏蔽掉很多人,你的价值就会体现,就不至于那么卷了。