# 泛型

软件工程中,我们不仅要创建定义良好且一致的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验)

泛型:不预先确定的数据类型,具体的类型在使用的时候才能确定。

泛型变量(类型参数)是代表类型的参数。

# 泛型的好处

  • 函数和类可以支持多种类型,增加的程序的可扩展性
  • 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性
  • 灵活控制类型之间的约束

# 基础示例

// 定义一个打印函数 只支持字符串参数
function log(value: string): string {
  console.log(value);
  return value;
}

// 函数重载 参数支持字符串,字符串数组
function log(value: string): string
function log(value: string[]): string[]
function log(value: any) {
  console.log(value);
  return value;
}

// 联合类型 参数支持字符串,字符串数组
function log(value: string | string[]): string | string[] {
  console.log(value);
  return value;
}

// 使用any类型
function log(value: any): any {
  console.log(value);
  return value;
}

使用 any 类型会导致这个函数可以接收任何类型的 arg 参数,但是这样就丢失了一些信息:忽略了输入参数类型和返回值类型必须一致,当调用者看到 log 函数时完全无法获知这种 约束关系

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。这里,我们使用了 类型参数,它是一种特殊的变量,只用于表示 类型 而不是

泛型函数

function log<T>(value: T): T  {
  console.log(value);
  return value;
}
log<string[]>(['a', ',b', 'c'])
log(['a', ',b', 'c'])

这个版本的 log 函数叫做 泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

log<string[]>(['a', ',b', 'c'])

第二种方法更普遍。利用了类型推断 -- 即编译器会根据传入的参数自动地帮助我们确定 T 的类型:

log(['a', ',b', 'c'])

# 泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

// 泛型函数
function log<T>(value: T): T  {
  console.log(value);
  return value;
}

// 使用泛型定义函数类型
let myLog: <T>(arg: T) => T = log

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

// 泛型函数
function log<T>(value: T): T {
  console.log(value);
  return value;
}

// 使用泛型定义函数类型
let myLog: <U>(arg: U) => U = log

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

// 泛型函数
function log<T>(value: T): T {
  console.log(value);
  return value;
}

// 使用泛型定义函数类型
let myLog: {<T>(arg: T) : T} = log

# 泛型接口

泛型约束了函数接口

interface Log{
  <T>(arg: T) : T
}

function log<T>(value: T): T {
  console.log(value);
  return value;
}

let myLog: Log = log

上面的例子泛型仅仅约束了一个函数,我们甚至可以把泛型参数当作整个接口的一个参数,来约束接口的其他成员。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string> 而不只是 Dictionary )。这样接口里的其它成员也能知道这个参数的类型了。

当泛型变量约束了整个接口时,在实现时必须指定类型。或在接口定义时指定一个默认类型。

interface Log<T> {
  (arg: T): T
}

// 指定默认string 类型的泛型接口
interface Log2<T = string> {
  (arg: T): T
}

function log<T>(value: T): T {
  console.log(value);
  return value;
}

// 必须指定类型
let myLog1: Log<string> = log

let myLog2: Log<number> = log

// 使用默认类型
let myLog3: Log2 = log

# 泛型类

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

实例化时可以指定类型,相应的方法,属性会受到泛型约束。

class Log<T> {
  // 静态成员不能引用类类型参数。
  // static run(value: T) {
  //   console.log(value)
  //   return value
  // }

  run(value: T) {
    console.log(value)
    return value
  }
}

// 实例化时可以指定类型
let log1=new Log<number>()
// 泛型约束函数参数
log1.run(11)

// 实例化没有指定参数
let log2 = new Log()
// 函数调用可以传递任意类型参数
log2.run(11)
log2.run('111')

# 泛型约束

在泛型函数内部使用类型变量时, 由于事先并不知道它是那种类型, 所以不能随意操作它的属性和方法:

function log<T>(value: T): T {
  // 类型“T”上不存在属性“length”。
  console.log(value.length);
  return value;
}

上述函数中 类型 T 上不一定存在 length 属性, 所以编译的时候就报错了。

这时,我们可以的对泛型进行约束,对这个函数传入的值约束必须包含 length 的属性, 这就是泛型约束:

interface Length{
  length:number
}

function log<T extends Length>(value: T): T {
  console.log(value.length);
  return value;
}

log('111')
// 类型“11”的参数不能赋给类型“Length”的参数。
log(11)

我们定义一个接口来描述约束条件,创建一个包含 .length 属性的接口,使用这个接口和 extends 关键字来实现约束。

# 在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。

function getProperty<T, K extends keyof T> (obj: T, key: K ) {
  return obj[key]
}

let x = {a: 1, b: 2, c: 3, d: 4}

getProperty(x, 'a') // okay
getProperty(x, 'm') // error

# 总结

泛型不仅可以保持类型的一致性,又不失程序的灵活性,同时也可以通过泛型约束,控制类型之间的约束。从代码的上来看,可读性,简洁性,远优于函数重载,联合类型声明以及 any 类型的声明。

# 参考

更新时间: 5/13/2020, 11:23:00 PM