第七节: TypeScript 泛型

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<T>(arg: T): T {
    return arg
}

此时,通过参数类型T将参数类型和返回类型建立了关系,

向函数添加了一个类型变量:T, 这里的T只是一个变量,用于捕获用户提供的类型, 以便我们可以使用该信息.

此时T再次使用作为返回类型, 此时函数的参数和返回类型使用了相同类型T, 这样Type捕获到用户传入的是什么类型, 那么函数返回的也将是什么类型

这时我么也可以说identity函数是一个通用函数, 因为它适用于多种类型.


1.3 泛型的调用

一旦我们编写了通用标识函数,我们就可以通过两种方式之一调用它

第一种方法是将所有的参数(包括参数类型)传递给函数

例如:

// <>中的Type 是一个类型变量
function identity<Type>(arg:Type):Type{
    return arg
}

// <> 中的类型是明确告知函数,此时调用时类型变量所代表的类型
const x = identity<string>('hello')
console.log(x)
// const x:string

在这里,我们明确的将Type设置为string类型, 表示函数的参数和返回值都是string类型

类型变量和传递明确类型使用<>而不是()


第二种方式也是最常见的,就是使用TypeScript的类型参数推断,

也就是说,我们不明确指定Type的类型,而是希望TypeScript编译器根据我们传入的参数类型自动推断并设置Type的类型,

例如:

function identity<Type>(arg:Type):Type{
  return arg
}

const x = identity('hello')
console.log(x)
// const x:string

这里并不必在尖括号(<>)中显示的传递类型, 编译器只是查看参数'hello'值并推断值的类型,然后将Type设置为此类型

虽然类型推断可以成为保持代码更短和更具可读性的有用工具, 但是当编译器无法推断类型时, 就可能需要我们使用方法一的方式, 显示的传递类型参数.


2. 使用泛型参数属性

当你开始使用泛型时, 你会注意到, 当你创建像identity这类泛型函数时, 编译器将会强制你正确的使用函数中任何泛型类型参数, 也就是说,你实际上将这些参数看做为可以是任何类型,因为类型参数可以接收任意类型

我们依然使用之前的identity函数, 但此时我们还想在arg参数每次调用时将参数的长度打印到控制台,我们可能会想这样处理

function identity<Type>(arg:Type):Type{
    console.log(arg.length)
    // Type类型上不存在length属性
    return arg
}

当我们这样做的时候,TypeScript就会给我们一个错误, 告诉我们Type类型上没有length属性.

请记住,我们之前说过这些类型变量,如Type代表任何类型, Type的具体类型,取决于调用时传递的类型或指定的类型

因此在使用函数的人很有可能传入number类型, 而number类型的值是没有length属性的

const x = identity<number>(20)


假设我们此时打算让这个函数作用于数组Type(就是数组中每一项是Type类型), 而不是Type类型直接作用于数组(就是Type类型就是数组本身).

简单说,我们希望的是 Type[]指代number[]或者string[], 也有可能是其他类型数组, 而此时Type的类型可能是number,string

我们不希望的是Type直接指代number[]

由于我们真正使用数组,因此参数的length属性就可以使用了,

那么我们就可以像创建其他类型数组一样来描述它:

例如:

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

此时你可以将类型解读泛型函数identity接受一个类型参数Type, 和一个普通的函数参数, 该参数arg是一个Type类型的数组, 并返回一个Type类型的数组.

比如,我们给函数参数传入一个数字数组(即number[]), 我们也将返回一个number[]

此时Type类型绑定到了number类型, 这表示TypeScript允许我们将泛型的类型变量Type用作我们正在使用类型的一部分, 而不是整个类型,从而为我们提供了更大的灵活性

上述的示例我们也可以如下编写

function identity<Type>(arg:Array<Type>):Array<Type>{
  console.log(arg.length)
  return arg
}

其实就是函数类型的写法以及简写方式


3. 泛型类型

在前面的部分中,我们创建了适用于一系列类型的通用标识函数。在本节中,我们将探讨函数本身的类型以及如何创建通用接口。

泛型函数的类型和非泛型函数的类型 一样, 类型参数先列出来, 类似于函数声明

// 泛型函数
function identity<Type>(arg:Type):Type{
  return arg
}

// 使用泛型函数的类型来对变量进行类型注释
let myIdentity:<Type>(arg:Type) => Type = identity


我们也可以为类型中的泛型类型参数使用不同的名称, 只要类型变量的数量和类型变量的使用方式保持一致

// 泛型函数
function identity<Type>(arg:Type):Type{
  return arg
}

// 使用泛型函数的类型来对变量进行类型注释
let myIdentity:<Input>(arg:Input) => Input = identity

从本质上来将, Type也好, Input也好其实就是类型的变量而已, 就像我们定义的普通变量一样, 不管定义什么名字, 只要保证使用的地方对了就行了

例如示例中,<Input>(arg:Input) => Input只是对于变量myIdentity做类型注释,表面myIdentity是一个函数,

并且函数的参数类型,返回值类型 使用泛型类型变量, 也就是类型保持同步,


我们还可以将泛型类型注释改写成对象字面量类型的调用签名, 这个调用签名前面章节已经了解过了

// 泛型函数
function identity<Type>(arg:Type):Type{
  return arg
}

// 使用对象字面量类型的调用签名进行类型注释
let myIdentity:{<Type>(arg:Type):Type} = identity


此时我们也可以将对象 字面量类型移动 到接口中,此时就成了通用接口

// 通用接口
// 接口中定义调用签名
interface GenericIdentity{
  <Type>(arg:Type):Type
}

// 泛型函数
function identity<Type>(arg:Type):Type{
  return arg
}

// 使用通用接口进行类型注释
let myIdentity:GenericIdentity = identity


在实例中,我们也可以将泛型参数移动为整个接口的参数, 这让我们看到了泛型的类型,对于整个接口的其他成员都是可见的

// 通用接口
// 接口中定义调用签名
// 将泛型参数移动为接口参数
interface GenericIdentity<Type>{
    (arg:Type):Type
}

// 泛型函数
function identity<Type>(arg:Type):Type{
  return arg
}

// 使用通用接口进行类型注释
// 此时在使用通用接口进行类型注释的时候就需要传递接口类型参数
let myIdentity:GenericIdentity<number> = identity

请注意,此时 我们的示例已经被更改的略有不同, 我们现在有了一个非泛型函数签名, 他是泛型接口的一部分, 而不是描述泛型函数

当我们使用GenericIdentity时, 我们还需要制定相应的类型参数,示例中传入了number类型

从而有效的锁定底层调用签名时将要使用的内容.

我们 需要了解,何时将类型参数放在调用签名上, 何时将其放 接口上, 放在什么位置有助于描述类型在那些方面

除了泛型接口外,我们还可以创建泛型类


4. 泛型类

泛型类具有与泛型接口相似的形状, 泛型类在类的名称后面的尖括号(<>)中有一个泛型类型参数列表

例如

// 通用类
class GenericNumber<NumType>{
    value: NumType
    add:(x:NumType,y:NumType) => NumType
}

//  使用通用类
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.value = 30
myGenericNumber.add = function(x,y){
    return x + y
}

myGenericNumber.add(10,20)


此时我们在 使用GenericNumber类时,没有什么限制它只能使用number类型, 我们也可以使用string类型,或更加复杂的对象

//  通用类使用字符串类型 
let myGenericNumber = new GenericNumber<string>()
myGenericNumber.value = 'hello'
myGenericNumber.add = function(x,y){
  return x + y
}

myGenericNumber.add('hello ','world')

请注意, 正如class类中介绍的那样, 一个类的类型有两方面:静态方面和实例方面, 泛型类仅在其实例方面而非其静态方面是通用的

因此要注意在使用类时, 静态成员不能使用类的类型参数


5. 通用约束

如果我们要编写了适用于一组类型的泛型函数, 你知道该组函数将具有哪些功能, 我们在示例中希望能够访问参数的length属性, 但是编译器无法证明每种类型都有length属性, 因此,TypeScript报错

function identity<Type>(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<Type extends Lengthwise>(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<Type,Key extends keyof Type>(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<T>(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<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
// function createInstance<Lion>(c: new () => Lion): Lion

createInstance(Bee).keeper.hasMask;
// function createInstance<Bee>(c: new () => Bee): Bee
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354