【TypeScript】从头开始梳理泛型(一)

210 阅读4分钟

介绍

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

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

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:

  • 类的实例成员
  • 类的方法
  • 函数参数
  • 函数返回值

创建泛型

举个🌰 创建一个泛型的例子,这个函数会返回任何传入它的值 不用泛型的写法

function hello(arg: number): number {
	return arg
}

或者用any类型来定义函数

function hello(arg: any): any {
	return arg
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要用泛型来确保返回值的类型与传入参数的类型是相同的,这里我们使用类型变量,它是一种特殊的变量,只用于表示类型

function hello<T>(arg: T): T {
	return arg
}

hello函数添加了类型变量T,就可以捕获传入的类型,我们就可以使用这个类型,之后我们再次使用了T当做返回值类型

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

使用泛型

  1. 传入所有的参数,包含类型参数
let output = hello<string>('hello')

function fine<T, U>(value: T, msg: U): T {
	return value
}

let out = fine<number,string>(12,'haichen')
  1. 利用类型推论,编译器会根据传入的参数自动地帮助我们确定T的类型
let output = hello('haichen')

我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。

使用泛型变量

如果我们想打印参数的长度,我们会这样做

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

这么做,编译器会报错说我们的arg的.length属性,但是没有地方指明arg具有这个属性 改造一下,操作T类型的数组

function logHello<T>(arg: T[]): T[] {
	console.log(arg.length)
    return arg
}

所以,需要我们这样理解:泛型函数logHello,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性

泛型类型

创建泛型接口

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

function hello<T>(arg: T): T {
    return arg;
}

let myHello: <T>(arg: T) => T = hello;

tips 函数表达式会写成这样:

let myHello = function(x: number,y: number):number {
	return x+y
}

这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 myHello,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 myHello 添加类型,则应该是这样:

let myHello: (x: number,y:number) => function(x: number,y:number):number {
	return x+y
}

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 => 在typeScript的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边> 是输出类型。

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口

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

function hello<T> (arg: T): T {
  return arg
}

let myHello: GenericHello = hello

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

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

function hello<T>(arg: T): T {
	return arg
}

let myHello: GenericHello<number> = hello

泛型类

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

class GernericNumber<T> {
	value: T;
    add: (x: T, y: T) => T
}

let myGerneric = new GernericNumber<number>()
myGerneric.value = 0;
myGerneric.add = function(x,y) { return x+y }

tips

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