前端开发系列045-基础篇之TypeScript语言特性(五)

本文主要对TypeScript中的泛型进行展开介绍。主要包括以下内容

❏ 泛型函数类型
❏ 泛型接口(Interface)
❏ 泛型类(Class)
❏ 泛型约束

一、泛型函数的类型

在以前的文章中,我们已经介绍了什么是泛型函数,它跟普通函数还是有些区别的(泛型函数使用类型变量来占位,具体类型值由函数调用传参决定)。以前文章中介绍过TypeScript中的数据类型,以及可选的类型声明。虽然并没有必要(因为可以通过类型推导机制推导出来),但我们确实能够抽取出普通函数的具体类型。下面代码中demo函数的函数类型为:(name:string,age:number) => string

//文件路径 ../08-泛型函数/03-函数的类型.ts

//[001] 函数的类型
//(1) 声明demo函数
function demo(name:string,age:number):string
{
  return "姓名:" +name + "年龄:" + age;
}

//(2) 把demo函数赋值给f
let f:(name:string,age:number)=>string = demo;
//使用demo函数的调用签名
//let f:{(name:string,age:number):string} = demo;
console.log(f("zs",18));    //姓名:zs年龄:18

接下来,我们花点时间研究,泛型函数的函数类型。其实泛型函数的类型与非泛型函数的类型本质上并没由什么不同,只是在最前面增加一个类型变量参数而已。下面给出具体的代码示例。

function demoT<T>(arg:T):T{
  return arg;
}
//泛型函数demoT的类型为:<T>(arg:T) =>T
let f1 : <T>(arg:T) =>T = demoT;
//使用带有调用签名的对象字面量来定义泛型函数
let f2 : {<T>(arg:T) :T} = demoT;
//可以使用不同的泛型参数名(这里为X)
let f3 : <X>(arg:X) =>X = demoT;
//不使用类型声明
let f4 = demoT;

console.log(f1("abc"));     //abc
console.log(f2("哈哈"));     //哈哈
console.log(f3("嘿嘿"));     //嘿嘿
console.log(f4("咕噜"));     //咕噜
泛型函数的类型声明可以使用不同的泛型参数,只要数量和使用方式一致即可。

二、泛型接口(Interface)

接口(Interface)指在面向对象编程语言中,不包含数据和逻辑但使用函数签名定义行为的抽象类型。

TypeScript提供了接口特性,TypeScript的接口可以定义数据和行为,也可以扩展其它接口或者类。

在传统面向对象编程范畴中,一个类可以被扩展为另外一个类,也可以实现一个或多个接口。实现某个接口可以被看做是签署了一份协议,接口相当于协议,当我们签署协议(实现接口)后,就必须遵守它的规则。

接口本身是抽象类型,其内容(规则)就是属性和方法的签名。

在前文中我们定义了泛型函数demoT,可以把demoT函数的签名抽取并定义接口GenericFn,下面给出示例代码。

//文件路径 ../08-泛型函数/04-泛型接口.ts

//(1) 声明泛型函数demoT
function demoT<T>(arg:T):T{
  return arg;
}

//(2) 定义GenericFn接口
interface GenericFn{
    <T>(arg: T): T;
}

let fn: GenericFn = demoT;
console.log(fn("哈哈"));  //哈哈

有时候,我们可能需要把泛型参数(T)抽取成为整个接口的参数,好处是抽取后我们能够清楚的知道使用的具体泛型类型是什么,且接口中的其它成员也能使用。当我们使用泛型接口的时候,传入一个类型参数来指定泛型类型即可,下面给出调整后的示例代码。

//文件路径 ../08-泛型函数/05-泛型接口02.ts

//(1) 声明泛型函数demoT
function demoT<T>(arg:T):T{
  return arg;
}

//(2) 定义泛型接口
interface GenericFn<T>{
    (arg: T): T;
}

let f1: GenericFn<number> = demoT;
console.log(f1(123));       //123
//报错:Argument of type '"字符串"' is not assignable to parameter of type 'number'.
//console.log(f1("字符串")); //错误的演示

let f2: GenericFn<string> = demoT;
console.log(f2("字符串")); //字符串

三、泛型类(Class)

泛型特性可以应用在Class身上,具体的使用方式和接口差不多。

//文件路径 ../08-泛型函数/06-泛型类.ts

//泛型类(Class)
class Person<T>{
  //[1] 属性部分
  name:T;
  color:T;
  //[2] 方法部分
  add:(a:T,b:T)=>T;
}

//获取实例对象p1
var p1 = new Person<string>();
p1.name = "张三";

//报错: TS2322: Type '123' is not assignable to type 'string'.
//p1.name = 123;  错误的演示
p1.color = "Red";
p1.add = function(a,b){
  return a + b;
}
console.log(p1);                      //{name:"张三",color:"Red",...}
console.log(p1.add("ABC","-DEF"));    //ABC-DEF


//获取实例对象p2
var p2 = new Person<number>();
p2.name = 0;
p2.color = 1;
p2.add = function(a,b){
  return a + b;
}
console.log(p2.add(100,200));         //300

上面的代码提供了泛型类使用的简单示例,在定义泛型类的时候,只需要直接把泛型类型放在类名(这里为Person)后面即可,通过new调用类实例化的时候,以<类型>的方式传递,在Class中应用泛型可以帮助我们确认类中的很多属性都在使用相同的类型,且能够优化代码结构。

四、泛型约束

有时候,我们可能需要对泛型进行约束。下面的代码中我们声明了泛型函数fn,并在fn的函数体中执行console.log("打印length值 = " + arg.length);意在打印参数的长度。这份代码在编译的时候会报错,因为无法确定函数调用时传入的参数一定拥有length属性。

//文件路径 ../08-泛型函数/02-泛型函数使用注意点.ts
//说明 该泛型函数使用类型变量T来表示接收参数和返回值的类型
function fn<T>(arg:T):T{
  console.log("打印length值 = " + arg.length);
  return arg;
}
//报错:error TS2339: Property 'length' does not exist on type 'T'.
console.log(fn([1,2,3]));

其实相比于操作any所有类型的数据而言,在这里我们需要对参数类型进行限制,要求传入的参数能够拥有length属性,这种场景可以使用泛型约束。

理想中泛型函数fn的工作情况是:“只要传入的参数类型拥有指定的属性length,那么代码就应该正常执行。 为此,需要列出对于T的约束要求。下面,我们先定义一个接口来描述特定的约束条件。然后使用这个接口和extends关键字来实现泛型约束,代码如下:

//文件路径 ../08-泛型函数/07-泛型约束.ts

//[001] 定义用于描述约束条件的接口
interface hasLengthP
{
  length: number;
}

//[002] 声明fn函数(应用了泛型约束)
function fn<T extends hasLengthP>(arg:T):T
{

  console.log("打印length值 = " + arg.length);
  return arg
}

//[003] 调用测试
console.log(fn([1,2,3]));   //打印length值 = 3 [1,2,3];
console.log(fn({name:"zs",length:1})); //打印length值 = 1 对象内容

//说明:字符串会被转换为对象类型(基本包装类型)
console.log(fn("测试"));    //打印length值 = 2 测试

//报错:error TS2345: Argument of type '123' is not assignable to parameter of type 'hasLengthP'.
console.log(fn(123));   //错误的演示

上面代码中的fn泛型函数被定义了约束,因此不再是适用于任意类型的参数。我们需要传入符合约束类型的值,传入的实参必须拥有length属性才能运行。

泛型约束中使用多重类型

提示 当声明泛型约束的时候,我们只能够关联一种类型。但有时候,我们确实需要在泛型约束中使用多重类型,接下来我们研究下它的可能性和实现方式。

假设现在有一个泛型类型需要被约束,它只允许使用实现Interface_One和Interface_Two两个接口的类型,考虑应该如何实现?

//文件路径 ../08-泛型函数/08-泛型约束中使用多重类型01.ts

//定义接口:Interface_One和Interface_Two
interface Interface_One{
  func_One();
}

interface Interface_Two{
  func_Two();
}

//泛型类(泛型约束为Interface_One,Interface_Two)
class  classTest<T extends Interface_One,Interface_Two>
{
  propertyDemo:T;
  propertyDemoFunc(){
    this.propertyDemo.func_One();
    this.propertyDemo.func_Two();
  }
}

我们可能会像这样来定义泛型约束,然而上面的代码在编译的时候会抛出错误,也就是说我们不能在定义泛型约束的时候指定多个类型(上面的代码中我们指定了Interface_One和Interface_Two两个类型),如果确实需要设计多重类型约束的泛型,可以通过把多重类型的接口转换为一个超接口来处理,下面给出示例代码。

//文件路径 ../08-泛型函数/09-泛型约束中使用多重类型02.ts

//定义接口:Interface_One和Interface_Two
interface Interface_One{
  func_One();
}

interface Interface_Two{
  func_Two();
}

//Interface_One和Interface_Two成为了超接口,它们是Interface_T的父接口
interface Interface_T extends Interface_One,Interface_Two{};

//泛型类
class  classTest<T extends Interface_T>
{
  propertyDemo:T;
  propertyDemoFunc(){
    this.propertyDemo.func_One();
    this.propertyDemo.func_Two();
  }
}

let obj = {
  func_One:function(){
    console.log("func_One");
  },
  func_Two:function(){
    console.log("func_Two");
  }
}
//获取实例化对象classTestA
let classTestA = new classTest();
classTestA.propertyDemo = obj;
classTestA.propertyDemoFunc();    //func_One func_Two


//下面是错误的演示
let classTestB = new classTest();

//报错: Type '{ func_Two: () => void; }' is not assignable to type 'Interface_T'.
classTestA.propertyDemo = {
  func_Two:function(){
      console.log("func_Two_XXXX");
  }
};

备注:该文章所有的示例代码均可以点击在Github托管仓库获取

posted on 2022-12-12 10:02  文顶顶  阅读(23)  评论(0编辑  收藏  举报

导航