函数类型 -- Typescript基础篇(5)

1,622 阅读7分钟

函数作为js一等公民,在js中占有重要地位。ts为函数添加了额外的功能,使之更容易被使用。

ts中有Function类型,但使用Function,在调用函数时得不到很好的类型检测和提示(就这类似于之前提到的object和接口类型)

函数类型声明

在ts中函数类型声明有两种方式:函数声明和函数表达式(与js一致)。不同是需要指定参数和返回值的类型。

// 函数声明
function add(x: number, y: number): number {
  return x + y;
}

// 函数表示式
const myAdd = function (x: number, y: number): number {
  return x + y;
};

函数表示式也可以写成:

const myAdd: (x: number, y: number) => number = function (x, y) {
  return x + y;
};

在函数表示式指定类型后,ts会自动推断实际参数的xy的类型是number,返回值是string

并且函数类型声明(x: number, y: number) => number中参数名不必和所赋值的函数参数名相同,只要保证各个参数类型兼容即可。

const myAdd: (base: number, increment: number) => number = function (x, y) {
  return x + y;
};

// 参数类型不兼容,将报错
// Type '(x: string, y: number) => string' is not assignable to type '(base: string, increment: number) => number'.
// Type 'string' is not assignable to type 'number'.
const myAdd: (base: string, increment: number) => number = function (x, y) {
  return x + y;
};

实际上,我们并不一定要为函数返回值显示指定类型,ts会根据参数类型,和return语句推断出返回值类型。这就是ts强大类型推断功能。

// 返回值类型会被推断为number
function add(x: number, y: number) {
  return x + y;
}

// 类型不兼容,将报错
// Type 'number' is not assignable to type 'string'
const str:string=add(1,2)

可选参数

默认情况下,在调用函数时,ts会对每个参数进行检查,保证函数调用时的实参和函数声明时的形参的个数和类型都匹配。

function buildName(firstName: string, lastName: string) {
  return firstName + " " + lastName;
}

const result = buildName("Bob", "Adams");

// Expected 2 arguments, but got 1
const result2 = buildName("Bob"); // error, too few parameters
// Expected 2 arguments, but got 3
const result3 = buildName("Bob", "Adams", "Sr.");

ts为函数提供了可选参数,使用形式和interface的可选属性使用形式相似,在参数名后加?

function buildName(firstName: string, lastName?: string) {
  return firstName + " " + lastName;
}
const result = buildName("Bob");

可选参数必须排在必需参数之后,否则会报错:

// 报错
// A required parameter cannot follow an optional parameter
function buildName(firstName?: string, lastName: string) {
  return firstName + " " + lastName;
}

默认参数

我们可以为参数提供默认值,如果该参数被传入undefined,将使用默认值代替。同时,如果默认参数在必需参数之后,则会被ts自动识别为可选参数。

// c将会被标记为带默认值的可选参数
function buildLetter(a: string = "a", b: string, c: string = "c") {
  return a + b + c;
}

// a=First, b=Second, c=Third
buildLetter("First", "Second", "Third");

// a=First, b=Second, c=c
buildLetter("First", "Second");

// a=a, b=Srcond, c=c
buildLetter(undefined, "Second");

剩余参数

不管是可选参数还是默认参数,它们都只针对一个参数。而通过ES6解构赋值可以实现对函数剩余参数的收集。剩余参数会被ts标识为一组数量不定(从0到任意多个)的可选参数,所以剩余参数是数组类型,并且必须放在必需参数之后。

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

const names = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

This参数

由于js的this指向在函数调用时动态变化的,使得它能实现很多强大且灵活的功能。但是带来的代价是:判断this指向变得令人头疼。

const user = {
  devices: ["mobile", "laptop", "desktop"],
  createDevicePicker: function () {
    return function () {
      return this.devices.join(",");
    };
  },
};

const picker = user.createDevicePicker();
console.log(picker());

如上面这段代码,我们期待输出的是mobile,laptop,desktop字符串,但是实际结果是一个Cannot read property 'devices' of undefined错误。

这是因为createDevicePicker所返回的新函数被真正调用时,实际上内部this指向的是undefined(非严格模式下指向全局变量)。

通常建议在配置文件中打开noImplicitThis,ts会对具有any类型的this进行警告。

this-error-hinter

上面的例子,如果使用ES6的箭头函数可以解决this指向的问题,同时ts也能正确推断出this类型。

const user = {
  devices: ["mobile", "laptop", "desktop"],
  createDevicePicker: function () {
    return () => {
      return this.devices.join(",");
    };
  },
};

const picker = user.createDevicePicker();
console.log(picker());

this-hinter

本质上这还是js所带来的问题,与ts无关,对于this具体指向请查看其它相关文档,本节不再赘述。

ts允许我们显示声明this的指向,通过使用this作为函数的第一个参数。this参数只是用来做类型约束(编译时),实际this指向不一定是如此,依赖于具体实现(运行时)

function f(this: void) {
 // 不能在该函数内使用this变量
}

试想一种场景:我们有一个ui接口,它有一个onClick的方法用来收集回调方法,并且我们期望回调方法都不需要使用this访问元素内部数据:

interface UIElement {
  message: string;
  onClick: (onClick: (this: void, message: string) => void) => void;
}
const ulElement: UIElement = {
  message: "click message",
  onClick(callback) {
    callback(this.message);
  },
};

function callback(this: void, message: string) {
   console.log(message);
}

ulElement.onClick(callback);

// 如果callback指定了不兼容的this将会报错
// The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'UIElement'
function cb(this:UIElement,message:string) {
}
ulElement.onClick(cb);

如果我们期望回调函数需要访问元素内部数据,可以这样实现:

interface UIElement {
  message: string;
  onClick: (onClick: (this: UIElement) => void) => void;
}

const ulElement: UIElement = {
  message: "click message",
  onClick(callback) {
    callback.call(this);
  },
};

function callback(this: UIElement) {
  console.log(this.message);
}

ulElement.onClick(callback);

重载

重载在其他面向对象的语言中是十分常见的功能,但是js不是那么容易实现此功能。

比如,我们现在有一个需求,有一个名为calculateLength的函数,它有两个参数:

  • 如果两个参数类型都是string,则返回字符串长度相加之和
  • 如果两个参数类型都是number,则返回两个数值相加之和
  • 没有其他组合形式

借助联合类型,我们可以按照如下方式实现:

function calculateLength(a: string | number, b: string | number): number {
  if (typeof a === "string" && typeof b === "string") {
    return a.length + b.length;
  } else if (typeof a === "number" && typeof b === "number") {
    return a + b;
  } else {
    return 0;
  }
}

但是我们实际调用函数时,以下方式都合法:

calculateLength("str1", "str2");
calculateLength(1, 3);

// 不符合规范的形式
calculateLength(1, "str2");
calculateLength("str1", 3);

可以看出只靠联合类型,无法精确表达需求。此时我们需要利用重载对参数组合进行限制:

function calculateLength(a: string, b: string): number;
function calculateLength(a: number, b: number): number;

function calculateLength(a: string | number, b: string | number): number {
  if (typeof a === "string" && typeof b === "string") {
    return a.length + b.length;
  } else if (typeof a === "number" && typeof b === "number") {
    return a + b;
  } else {
    return 0;
  }
}

calculateLength("str1", "str2");
calculateLength(1, 3);

// 此时对于不符合规范的参数组合会报错
// No overload matches this call.
calculateLength(1, "str2");
calculateLength("str1", 3);

可以看到,我们重复声明了多次calculateLength函数,除了最后一次是具体的函数实现,前面都只是函数类型声明。

ts在判断函数调用是否符合规范时,会按照重载声明的顺序从前向后依次匹配,直至找到为止。如果重载声明具有包含关系,需要优先把最精确的声明放在前面。

在上面的例子中,如果重载声明为:

function calculateLength(a: any, b: any): number;
function calculateLength(a: string, b: string): number;
function calculateLength(a: number, b: number): number;

任何函数调用都会被匹配到第一个函数声明,所有实参类型组合都会被认为合法。

也可使用接口表示重载:

interface CalculateLength {
  (a: string, b: string): number;
  (a: number, b: number): number;
}

const fn: CalculateLength = (
  a: string | number,
  b: string | number
): number => {
  if (typeof a === "string" && typeof b === "string") {
    return a.length + b.length;
  } else if (typeof a === "number" && typeof b === "number") {
    return a + b;
  } else {
    return 0;
  }
};

void还是undefined

之前我们在介绍void类型时,提到如果函数没有显式的返回值,则返回类型是void。在js中,如果函数没有显式的返回值,会默认返回undefined。那可不可以用undefined作为这种情况的返回值呢?

return-undefined

可以看到此时ts会报错,它要求如果返回值类型不是any或者void,函数必须有一个显式的返回值。所以如果要返回值为undefined合法,必须在函数末尾加return undefined

这也可以解释为什么开启strictNullChecks后,undefined依旧可以赋值给void