探秘Typescript·函数类型的补充

72 阅读6分钟

探秘Typescript·函数类型的补充

函数类型的补充

前言

我们之前在学习Typescript日常类型时,有稍微提过一下函数类型的常规使用定义,但那只是函数类型的冰山一角,函数的完整类型相关的知识远不止这些,今天我们就来更深入地学习一下Typescript当中函数的类型描述吧。

构造函数的表达

我们都知道,除了一些纯粹的函数外,在Javascript还有一类特殊的函数,他们就是构造函数构造函数允许我们通过new关键字进行实例化,将一个构造函数实例化为对象进行使用。那么,我们在Typescript当中要如何定义构造函数呢?

type MyConstructor<T> = {
  new (len: number): T
}
function factory<T>(Ctor: MyConstructor<T>, len: number) {
  return new Ctor(len);
}

const arr = factory<string[]>(Array, 100);
const date = factory<Date>("", 100);// 类型“string”的参数不能赋给类型“MyConstructor<Date>”的参数。

通过上述方式,我们就可以使用Typescript的类型校验能力校验传入的参数是否支持实例化。

泛型与函数

我们定义函数时,通常一个函数所接受的参数或其返回值取决于实际使用的类型,在定义时无法准确的描述。那么,这种情况我们可以使用泛型搭配函数使得我们的函数灵活度更高,如:

type BaseRequestParams = {};
type BaseResponse<T> = {
  code: number;
  msg: string;
  data: T
};

async function request<Props extends BaseRequestParams, ResponseType extends BaseResponse>(params: Props): Promise<ResponseType> {
  console.log(params);
  return {
    code: 0,
    msg: "success"
  } as ResponseType;
}

类型推导与泛型的特化

function map<Input, Output>(arr: Input[], cb: (item: Input) => Output): Output[] {
  return arr.map(cb);
}
const typeList: string[] = ['AW', 'AB', 'SQ', 'TR', 'LB', 'CR', 'MA'];
const res = map(typeList, item => item.length);// res 的类型为 number

image-20220925111438150

image-20220925111458420

从上面的示例可以看出,我们在实际使用map时,并没有给map传递泛型参数,但是,聪明的Typescript能够根据我们传入的类型,自动推导出InputOutput的类型出来。这种场景就是泛型的特化。在我们还没有传入实际的值之前的InputOutput是还没有被特化的泛型,而一旦我们传入了具体的值,该泛型则会根据我们实际传入的类型特化成对应的类型。

泛型参数规范

在非必要时,泛型参数越少,代码可读性越高,因此,我们再进行函数的泛型参数定义时,如果可以使用一个泛型参数推导出来的,就不要定义多个泛型参数。

可选参数

使用?来描述可选参数,如:

function myForeach<T>(arr: T[], cb: (item: T: index?: number, arr: T[])): void {
  for(let i=0;i<arr.length;i++) {
    cb(arr[i], i, arr);
  }
}
myForeach([1,2,3], (item, i) => {
  // 可选参数在使用时需要加一个?,也就是可选链
  console.log(item, i?.toFixed(2));
})

函数的重载

大家先来思考一下,以下这段程序有没有意义:

function add<T>(a: T, b: T): T {
  return a + b;// 运算符“+”不能应用于类型“T”和“T”。
}

上面这段程序,可能设计之初十想要兼容stringnumer类型的加法,但实际上,泛型在定义时,并没办法确定类型,此时我们就使用加法运算很显然是不对的,难道当T的类型是一个对象时,我们也直接把对象想加吗?显然不符合期望的。

绝大部分时候,这类场景都是有限的类型才支持这些方法,那么此时,我们就可以利用函数的重载(overloading)解决这个问题了。


function isArray<T>(arr: unknown): arr is T[] {
  return Array.isArray(arr);
}

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add<T>(a: T[], b: T[]): T[];
// 如果 Typescript 发现上面有类型声明时,下面这一行实现的类型他就不关心了,所以其实下面这行这么写也是可以的,效果完全是等效的,并不会影响 Typescript 的类型校验:
// function add(a: any, b: any) {
//   if(isArray(a) && isArray(b)) {
function add<T>(a: T, b: T): T {
  if(isArray<T>(a) && isArray<T>(b)) {
    return [...a, ...b] as any;
  }
  // 这里之所以需要 as any,是因为我们此时 a 和 b 依然还是泛型类型,Typescript 并不知道运行时传过来的类型是怎样的,因此需要断言一下,由于上面我们利用函数重载已经做了足够的类型说明了,Typescript 可以根据这些类型说明进行类型的判断和推导了,所以此处用 any对 Typescript 的类型校验不会有负面的影响。
  return (a as any) + (b as any);
}

const a = add(1, 3);
const b = add('33', '44');
const c = add([1, 2, 3], [4, 5, 6]);
// 没有与此调用匹配的重载。
//   第 1 个重载(共 2 个),“(a: number, b: number): number”,出现以下错误。
//   类型“string”的参数不能赋给类型“number”的参数。
// 第 2 个重载(共 2 个),“(a: string, b: string): string”,出现以下错误。
//   类型“number”的参数不能赋给类型“string”的参数。
const d = add(1, '2');

如上述示例,我们对一个函数进行了多次不重叠的类型描述,这样,在我们后面使用时,就会有多个重载的函数供我们选择,我们可以传入纯数字进行想加,也可以传入存字符串进行拼接,但如果没有重载字符串与数字相加的方法,如你所见,将会直接报错。

需要特别注意的一个点就是,函数的重载类型描述必须与函数的实现放在一起,他们中间不能有任意其他的逻辑语句,不然会报错,如:

image-20220925140524744

总结一下:我们利用函数的重载来约束跨类型方法的使用,让程序可预测,行为可控。

操作符重载

实际上,无论是在Javascript还是Typescript中,本身是不支持操作符重载的,这个概念更多的是来自于C++当中操作符重载功能,该功能能让我们自定义实现操作符的功能,如:+-*/,就比如上述的函数重载功能中,如果支持操作符重载的话,我们完全可以这样实现:

const a = [1,2,3];
const b = [4,5,6];
const c = a + b; // [1,2,3,4,5,6];

虽然我们原生程序不支持操作符重载,但我们可以借助babel插件帮我们在编译阶段实现这个功能:babel-plugin-overload-operator

函数的 this 指针

在某些情况下,我们需要使用this关键字操作当前实例,那么此时我们可以这样定义:

function Person(this: Person, name: string) {
  console.log(`hi,my name is ${name}, I am ${this.age} years old!`)
}
const p = new Person("kiner");

总结

泛型帮助我们让类型检查变得更加严格,更加智能