1. 泛型理解
泛型是通过参数化类型来实现在同一份代码上操作多种数据类型。
1.1 未使用泛型
现在如果需要实现函数接受什么类型的数据就返回什么类型的数据,在不使用泛型的情况下,代码如下
function identity(num:number):number{
return num
}
上面的示例只能实现函数接受number
类型的实参,并且返回number
类型, 没法处理其他数据类型
也就是说如果想实现一个函数接受一个string
类型参数, 并返回string
类型, 上面的函数不能使用
当然我们也可以选择任意数据类型any,
代码如下
function identity(arg: any): any {
return arg
}
虽然感觉起来能够实现我们的需求, 函数传递什么数据类型, 就返回什么数据类型的值.
但是any类型会导致这个函数会丢失了一些信息:传入的类型与返回的类型并不一定相同的。
例如: 将代码改写一下
function identity(arg: any): any {
return arg + ''
}
这样的写法如果传入number
类型 返回string
类型, 编译的时候并不会报错,因为any类型可以是任意数据类型, 没有规定参数的any类型 和返回值any类型 必须保持一致.
1.2 使用泛型
我们需要一种捕获参数类型的方法,以便我们也可以使用它来表示返回的内容。在这里,我们将使用类型变量,一种特殊类型的变量,它作用于类型而不是值, 这就是我们所谓的泛型
简单说就像我们声明的变量或函数参数一样, 只不过变量和参数是用来接受值, 而泛型的参数类型是用来接受类型的
例如: 使用泛型实现上述功能
function identity(arg: T): T {
return arg
}
此时,通过参数类型T
将参数类型和返回类型建立了关系,
向函数添加了一个类型变量:T
, 这里的T
只是一个变量,用于捕获用户提供的类型, 以便我们可以使用该信息.
此时T
再次使用作为返回类型, 此时函数的参数和返回类型使用了相同类型T
, 这样Type
捕获到用户传入的是什么类型, 那么函数返回的也将是什么类型
这时我么也可以说identity
函数是一个通用函数, 因为它适用于多种类型.
1.3 泛型的调用
一旦我们编写了通用标识函数,我们就可以通过两种方式之一调用它
第一种方法是将所有的参数(包括参数类型)传递给函数
例如:
// <>中的Type 是一个类型变量
function identity(arg:Type):Type{
return arg
}
// <> 中的类型是明确告知函数,此时调用时类型变量所代表的类型
const x = identity('hello')
console.log(x)
// const x:string
在这里,我们明确的将Type
设置为string
类型, 表示函数的参数和返回值都是string类型
类型变量和传递明确类型使用<>
而不是()
第二种方式也是最常见的,就是使用TypeScript的类型参数推断,
也就是说,我们不明确指定Type
的类型,而是希望TypeScript编译器根据我们传入的参数类型自动推断并设置Type
的类型,
例如:
function identity(arg:Type):Type{
return arg
}
const x = identity('hello')
console.log(x)
// const x:string
这里并不必在尖括号(<>)中显示的传递类型, 编译器只是查看参数'hello'
值并推断值的类型,然后将Type
设置为此类型
虽然类型推断可以成为保持代码更短和更具可读性的有用工具, 但是当编译器无法推断类型时, 就可能需要我们使用方法一的方式, 显示的传递类型参数.
2. 使用泛型参数属性
当你开始使用泛型时, 你会注意到, 当你创建像identity
这类泛型函数时, 编译器将会强制你正确的使用函数中任何泛型类型参数, 也就是说,你实际上将这些参数看做为可以是任何类型,因为类型参数可以接收任意类型
我们依然使用之前的identity
函数, 但此时我们还想在arg
参数每次调用时将参数的长度打印到控制台,我们可能会想这样处理
function identity(arg:Type):Type{
console.log(arg.length)
// Type类型上不存在length属性
return arg
}
当我们这样做的时候,TypeScript就会给我们一个错误, 告诉我们Type
类型上没有length
属性.
请记住,我们之前说过这些类型变量,如Type
代表任何类型, Type
的具体类型,取决于调用时传递的类型或指定的类型
因此在使用函数的人很有可能传入number
类型, 而number
类型的值是没有length
属性的
const x = identity(20)
假设我们此时打算让这个函数作用于数组Type
(就是数组中每一项是Type类型), 而不是Type
类型直接作用于数组(就是Type类型就是数组本身).
简单说,我们希望的是 Type[]
指代number[]
或者string[]
, 也有可能是其他类型数组, 而此时Type
的类型可能是number
,string
我们不希望的是Type
直接指代number[]
由于我们真正使用数组,因此参数的length
属性就可以使用了,
那么我们就可以像创建其他类型数组一样来描述它:
例如:
function identity(arg:Type[]):Type[]{
console.log(arg.length)
return arg
}
此时你可以将类型解读泛型函数identity
接受一个类型参数Type
, 和一个普通的函数参数, 该参数arg
是一个Type
类型的数组, 并返回一个Type
类型的数组.
比如,我们给函数参数传入一个数字数组(即number[])
, 我们也将返回一个number[]
此时Type
类型绑定到了number
类型, 这表示TypeScript允许我们将泛型的类型变量Type
用作我们正在使用类型的一部分, 而不是整个类型,从而为我们提供了更大的灵活性
上述的示例我们也可以如下编写
function identity(arg:Array):Array{
console.log(arg.length)
return arg
}
其实就是函数类型的写法以及简写方式
3. 泛型类型
在前面的部分中,我们创建了适用于一系列类型的通用标识函数。在本节中,我们将探讨函数本身的类型以及如何创建通用接口。
泛型函数的类型和非泛型函数的类型 一样, 类型参数先列出来, 类似于函数声明
// 泛型函数
function identity(arg:Type):Type{
return arg
}
// 使用泛型函数的类型来对变量进行类型注释
let myIdentity:(arg:Type) => Type = identity
我们也可以为类型中的泛型类型参数使用不同的名称, 只要类型变量的数量和类型变量的使用方式保持一致
// 泛型函数
function identity(arg:Type):Type{
return arg
}
// 使用泛型函数的类型来对变量进行类型注释
let myIdentity:(arg:Input) => Input = identity
从本质上来将, Type
也好, Input
也好其实就是类型的变量而已, 就像我们定义的普通变量一样, 不管定义什么名字, 只要保证使用的地方对了就行了
例如示例中,(arg:Input) => Input
只是对于变量myIdentity
做类型注释,表面myIdentity
是一个函数,
并且函数的参数类型,返回值类型 使用泛型类型变量, 也就是类型保持同步,
我们还可以将泛型类型注释改写成对象字面量类型的调用签名, 这个调用签名前面章节已经了解过了
// 泛型函数
function identity(arg:Type):Type{
return arg
}
// 使用对象字面量类型的调用签名进行类型注释
let myIdentity:{(arg:Type):Type} = identity
此时我们也可以将对象 字面量类型移动 到接口中,此时就成了通用接口
// 通用接口
// 接口中定义调用签名
interface GenericIdentity{
(arg:Type):Type
}
// 泛型函数
function identity(arg:Type):Type{
return arg
}
// 使用通用接口进行类型注释
let myIdentity:GenericIdentity = identity
在实例中,我们也可以将泛型参数移动为整个接口的参数, 这让我们看到了泛型的类型,对于整个接口的其他成员都是可见的
// 通用接口
// 接口中定义调用签名
// 将泛型参数移动为接口参数
interface GenericIdentity{
(arg:Type):Type
}
// 泛型函数
function identity(arg:Type):Type{
return arg
}
// 使用通用接口进行类型注释
// 此时在使用通用接口进行类型注释的时候就需要传递接口类型参数
let myIdentity:GenericIdentity = identity
请注意,此时 我们的示例已经被更改的略有不同, 我们现在有了一个非泛型函数签名, 他是泛型接口的一部分, 而不是描述泛型函数
当我们使用GenericIdentity
时, 我们还需要制定相应的类型参数,示例中传入了number
类型
从而有效的锁定底层调用签名时将要使用的内容.
我们 需要了解,何时将类型参数放在调用签名上, 何时将其放 接口上, 放在什么位置有助于描述类型在那些方面
除了泛型接口外,我们还可以创建泛型类
4. 泛型类
泛型类具有与泛型接口相似的形状, 泛型类在类的名称后面的尖括号(<>
)中有一个泛型类型参数列表
例如
// 通用类
class GenericNumber{
value: NumType
add:(x:NumType,y:NumType) => NumType
}
// 使用通用类
let myGenericNumber = new GenericNumber()
myGenericNumber.value = 30
myGenericNumber.add = function(x,y){
return x + y
}
myGenericNumber.add(10,20)
此时我们在 使用GenericNumber
类时,没有什么限制它只能使用number
类型, 我们也可以使用string
类型,或更加复杂的对象
// 通用类使用字符串类型
let myGenericNumber = new GenericNumber()
myGenericNumber.value = 'hello'
myGenericNumber.add = function(x,y){
return x + y
}
myGenericNumber.add('hello ','world')
请注意, 正如class类中介绍的那样, 一个类的类型有两方面:静态方面和实例方面, 泛型类仅在其实例方面而非其静态方面是通用的
因此要注意在使用类时, 静态成员不能使用类的类型参数
5. 通用约束
如果我们要编写了适用于一组类型的泛型函数, 你知道该组函数将具有哪些功能, 我们在示例中希望能够访问参数的length
属性, 但是编译器无法证明每种类型都有length
属性, 因此,TypeScript报错
function identity(arg:Type):Type{
console.log(arg.length)
// 警告, Type上不具有length属性
return arg
}
我们不想使用任何类型, 而希望将此函数限制为使用具有length
属性的任何类型,
只要类型有了这个属性,至少 拥有这个属性, 我们就会允许它通过类型效验
为此我们必须将我们的要求列为Type
的限制条件
我们将创建一个具有length
属性的约束接口,然后我们使用extends
关键词继承该接口, 以此来表示我们的约束
例如:
// 具有length属性的接口
interface Lengthwish{
length:number
}
// 通过泛型类型参数extends继承具有length属性接口
// 此时调用函数传入的任意类型必须满足具有length属性, 以此达到约束的目的
function identity(arg:Type):Type{
console.log(arg.length)
return arg
}
因为泛型函数此时受到了接口的约束, 它将不再适用于任何类型
// 调用函数警告
identity(3);
// 警告: 类型number不能赋给具有Lenthwise类型的参数
相反的,我们传入参数的类型必须具有所有的必需属性的值
identity({length:10,value:3});
6 在泛型约束中使用类型参数
其实所谓的泛型约束就是通过extends
关键字, 继承来达到, 传入的类型必须满足extends后面的约束
而泛型约束中使用类型参数, 就是extends
关键字后面的约束条件中将使用另外一个类型参数
例如:我们想从一个给定名称的对象上获取一个属性, 我们想确保我们不会获取到对象上不存在的属性
我们将在两种类型参数之间放置一个约束条件
示例:
// 约束类型参数Key
function getProperty(obj:Type,key:Key){
return obj[key]
}
const x = {a:1,b:2,c:3}
getProperty(x,'a')
getProperty(x,'m')
// 警告:类型参数'm'不能赋值给'a'|'b'|'c'类型;
实例中keyof
关键字是TypeScript提供的用于获取对象类型键值组成的文字联合类型
例如:
interface Person {
name:string;
age: number;
}
type Num = keyof Person;
/*
鼠标移入Num看到的类型: type Num = keyof Person
其实Num的类型 是 Person类型key 组成的文字联合类型
也就是 'name' | 'age'
*/
// 条件判断 'name' 文字类型是否是Num类型的子类型, 是返回string类型, 否返回boolean类型
type A = 'name' extends Num ? string: boolean
// type Num = keyof Person
理解了keyof
关键字的作用, 前面的例子就好理解了
7. 在泛型中使用类类型
在TypeScript 中使用泛型创建工厂函数时, 我们需要通过其构造函数引用类类型,
例如:
// 泛型函数
// 泛型T 是类返回的对象
function create(c: {new(name:string,age:number):T}):T{
return new c('张三',18)
}
// 类
class Person{
name:string
age: number
constructor(name:string,age:number){
this.name = name;
this.age = age
}
}
let student = create(Person)
console.log('student', student)
一个更高级的示例使用原型属性来推断和约束构造函数和类类型的实例端之间的关系。
// BeeKeeper类
class BeeKeeper {
hasMask: boolean = true;
}
// ZooKeeper类
class ZooKeeper {
nametag: string = "Mikle";
}
// Animal类
class Animal {
numLegs: number = 4;
}
// Bee类继承 Animal类
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
// Lion 类继承 Animal类
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
// 函数接受一个类, 这个类的实例化后返回的A 要继承 Animal类
function createInstance(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
// function createInstance(c: new () => Lion): Lion
createInstance(Bee).keeper.hasMask;
// function createInstance(c: new () => Bee): Bee