阅读 657

TS入门篇 | 详解 TypeScript 函数类型

“这是我参与8月更文挑战的第13天,活动详情查看: 8月更文挑战


在 JavaScript 中,函数是构建应用的一块基石,我们可以使用函数抽离可复用的逻辑、抽象模型、封装过程。在TypeScript中,函数仍然是最基本、最重要的概念之一。下面就来看看TypeScript中的函数类型是如何定义和使用的。

1. 函数类型定义

(1)直接定义函数类型

函数类型的定义包括对参数返回值的类型定义:

function add(arg1: number, arg2: number): number {
  return x + y;
}
const add = (arg1: number, arg2: number): number => {
  return x + y;
};
复制代码

这里用function字面量箭头函数两种形式定义了add函数。函数参数 arg1 和 arg2 都是数值类型,最后通过相加得到的结果也是数值类型。

如果在这里省略参数的类型,TypeScript 会默认这个参数是 any 类型;如果省略返回值的类型,如果函数无返回值,那么 TypeScript 会默认函数返回值是 void 类型;如果函数有返回值,那么 TypeScript 会根据定义的逻辑推断出返回类型。

需要注意,在TypeScript中,如果函数没有返回值,并且我们显式的定义了这个函数的返回值类型为 undefined,那就会报错:A function whose declared type is neither 'void' nor 'any' must return a value。正确的做法就是上面说的,将函数的返回值类型声明为void:

function fn(x: number): void {
	console.log(x)
}
复制代码

一个函数的定义包括函数名、参数、逻辑和返回值。为函数定义类型时,完整的定义应该包括参数类型和返回值类型。上面都是在定义函数的指定参数类型和返回值类型。下面来定义一个完整的函数类型,以及用这个函数类型来规定一个函数定义时参数和返回值需要符合的类型。

let add: (x: number, y: number) => number;
add = (arg1: number, arg2: number): number => arg1 + arg2;
add = (arg1: string, arg2: string): string => arg1 + arg2; // error
复制代码

这里定义了一个变量 add,给它指定了函数类型,也就是(x: number, y: number) => number,这个函数类型包含参数和返回值的类型。然后给 add 赋了一个实际的函数,这个函数参数类型和返回类型都和函数类型中定义的一致,所以可以赋值。后面又给它赋了一个新函数,而这个函数的参数类型和返回值类型都是 string 类型,这时就会报如下错误:

不能将类型"(arg1: string, arg2: string) => string"分配给类型"(x: number, y: number) => number"。
参数"arg1""x" 的类型不兼容。
不能将类型"number"分配给类型"string"复制代码

注意: 函数中如果使用了函数体之外定义的变量,这个变量的类型是不体现在函数类型定义的。

(2)接口定义函数类型

使用接口可以清晰地定义函数类型。下面来使用接口为add函数定义函数类型:

interface Add {
  (x: number, y: number): number;
}
let add: Add = (arg1: string, arg2: string): string => arg1 + arg2; 
// error 不能将类型“(arg1: string, arg2: string) => string”分配给类型“Add”
复制代码

通过接口形式定义了函数类型,这个接口Add定义了这个结构是一个函数,两个参数类型都是number类型,返回值也是number类型。当指定变量add类型为Add时,再要给add赋值,就必须是一个函数,且参数类型和返回值类型都要满足接口Add,显然这个函数并不满足条件,所以报错了。

(3)类型别名定义函数类型

可以使用类型别名来定义函数类型,这种形式更加直观易读:

type Add = (x: number, y: number) => number;
let add: Add = (arg1: string, arg2: string): string => arg1 + arg2; 
// error 不能将类型“(arg1: string, arg2: string) => string”分配给类型“Add”
复制代码

使用type关键字可以给任何定义的类型起一个别名。上面定义了 Add 这个别名后,Add就成为了一个和(x: number, y: number) => number一致的类型定义。上面定义了Add类型,指定add类型为Add,但是给add赋的值并不满足Add类型要求,所以报错了。

注意,这里的=>与 ES6 中箭头函数的=>不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。

2. 函数参数定义

(1)可选参数

TypeScript 会在编写代码时就检查出调用函数时参数中存在的一些错误:

type Add = (x: number, y: number) => number;
let add: Add = (arg1, arg2) => arg1 + arg2;
add(1, 2);    // success
add(1, 2, 3); // error 应有 2 个参数,但获得 3 个
add(1);       // error 应有 2 个参数,但获得 1 个
复制代码

在JavaScript中,上面代码中后面两个函数调用都不会报错, 只不过add(1, 2, 3)可以返回正确结果3add(1)会返回NaN。而在TypeScript中我们设置了指定的参数,那么在使用该类型时,传入的参数必须与定义的参数类型和数量一致。

但有时候,函数有些参数不是必须的,我们就可以将函数的参数设置为可选参数。可选参数只需在参数名后跟随一个?即可:

type Add = (x: number, y: number, z?: number) => number;
let add: Add = (arg1, arg2, arg3) => arg1 + arg2 + arg3;
add(1, 2);    // success   3
add(1, 2, 3); // success   6
复制代码

上面的代码中,z是一个可选参数,那他的类型就是number | undefined,表示参数 z 就是可缺省的,那是不是意味着可缺省和类型是 undefined 等价呢?来看下面的例子:

function log(x?: number) {
  console.log(x);
}
function log1(x: number | undefined) {
  console.log(x);
}
log();
log(undefined);
log1();    // Expected 1 arguments, but got 0
log1(undefined);
复制代码

可以看到,第三次函数调用报错了,这里的 ?: 表示在调用函数时可以不显式的传入参数。但是,如果声明了参数类型为 number | undefined,就表示函数参数是不可缺省且类型必须是 number 或者 undfined。

需要注意,可选参数必须放在必选参数后面,这和在 JS 中定义函数是一致的。来看例子:

type Add = (x?: number, y: number) => number;  // error 必选参数不能位于可选参数后。
复制代码

在TypeScript中,可选参数必须放到最后,上面把可选参数x放到了必选参数y前面,所以报错了。在 JavaScript 中是没有可选参数这个概念的,只不过在编写逻辑时,可能会判断某个参数是否为undefined,如果是则说明调用该函数的时候没有传这个参数,要做下兼容处理;而如果几个参数中,前面的参数是可不传的,后面的参数是需要传的,就需要在该可不传的参数位置传入一个 undefined 占位才行。

(3)默认参数

在 ES6 标准出来之前,默认参数实现起来比较繁琐:

var count = 0;
function counter(step) {
  step = step || 1;
  count += step;
}
复制代码

上面定义了一个计数器增值函数,这个函数有一个参数 step,即每次增加的步长,如果不传入参数,那么 step 接受到的就是 undefined,undefined 转换为布尔值是 false,所以 step || 1 这里取了 1,从而达到了不传参数默认 step === 1 的效果。

在 ES6 中,定义函数时给参数设默认值直接在参数后面使用等号连接默认值即可:

const count = 0;
const counter = (step = 1) => {
  count += step;
};
复制代码

当为参数指定了默认参数时,TypeScript 会识别默认参数的类型;当调用函数时,如果给这个带默认值的参数传了别的类型的参数则会报错:

const add = (x: number, y = 2) => {
  return x + y;
};
add(1, "ts"); // error 类型"string"的参数不能赋给类型"number"的参数
复制代码

当然也可以显式地给默认参数 y 设置类型:

const add = (x: number, y: number = 2) => {
  return x + y;
};
复制代码

注意:函数的默认参数类型必须是参数类型的子类型,如下代码:

const add = (x: number, y: number | string = 2) => {
  return x + y;
};
复制代码

这里 add 函数参数 y 的类型为可选的联合类型 number | string,但是因为默认参数数字类型是联合类型 number | string 的子类型,所以 TypeScript 也会检查通过。

(3)剩余参数

在 JavaScript 中,如果定义一个函数,这个函数可以输入任意个数的参数,那么就无法在定义参数列表的时候挨个定义。在 ES6 发布之前,需要用 arguments 来获取参数列表。arguments 是一个类数组对象,它包含在函数调用时传入函数的所有实际参数,它还包含一个 length 属性,表示参数个数。下面来模拟实现函数的重载:

function handleData() {
  if (arguments.length === 1) return arguments[0] * 2;
  else if (arguments.length === 2) return arguments[0] * arguments[1];
  else return Array.prototype.slice.apply(arguments).join("_");
}
handleData(2); // 4
handleData(2, 3); // 6
handleData(1, 2, 3, 4, 5); // '1_2_3_4_5'
复制代码

这段代码如果在TypeScript环境中执行,三次handleData的调用都会报错,因为handleData函数定义的时候没有参数。

在 ES6 中,加入了拓展运算符,它可以将一个函数或对象进行拆解。它还支持用在函数的参数列表中,用来处理任意数量的参数:

const handleData = (arg1, ...args) => {
  console.log(args);
};
handleData(1, 2, 3, 4, 5); // [ 2, 3, 4, 5 ]
复制代码

在 TypeScript 中可以为剩余参数指定类型:

const handleData = (arg1: number, ...args: number[]) => {

};
handleData(1, "a"); // error 类型"string"的参数不能赋给类型"number"的参数
复制代码

3. 函数重载

JavaScript 作为一个动态语言是没有函数重载的,只能自己在函数体内通过判断参数的个数、类型来指定不同的处理逻辑:

const handleData = x => {
  if (typeof x === 'string') {
    return Number(x);
  }
  if (typeof x === 'number') {
    return String(x);
  }
  return -1;
};
handleData(996)   // "996"
handleData("996") // 996
handleData(null)  // -1
复制代码

可以看到传入的参数类型不同,返回的值的类型是不同的,我们传入了三个类型的值作为参数,得到的结果都会被推断成 string | number 。

那有没有一种办法可以更精确地描述参数与返回值类型约束关系的函数类型呢? 这就是函数重载。TypeScript 中有的函数重载并不是定义几个同名实体函数,然后根据不同的参数个数或类型来自动调用相应的函数。而是在类型系统层面的,是为了更好地进行类型推断。TypeScript的函数重载通过为一个函数指定多个函数类型定义,从而对函数调用的返回值进行检查

const handleData = (x: string): number;
const handleData = (x: number): string;
const handleData = (x: null): number;
const handleData = (x: string | number | null): any => {
  if (typeof x === 'string') {
    return Number(x);
  }
  if (typeof x === 'number') {
    return String(x);
  }
  return -1;
};
handleData(996)   // "996"
handleData("996") // 996
handleData(false)  // error
复制代码

首先使用function关键字定义了三个同名的函数,这两个函数没有实际的函数体逻辑,而是只定义函数名、参数及参数类型以及函数的返回值类型。第四个使用function定义的同名函数,是一个完整的实体函数,包含函数名、参数及参数类型、返回值类型和函数体。这四个定义组成了一个完整的带有类型定义的函数,前两个function定义的就称为函数重载,而第四个function并不算重载。

在调用 handleData 函数时,TypeScript 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义。因此,我们需要把最精确的函数重载放到前面。

来看下匹配规则,当调用这个函数并且传入参数的时候,会从上而下在函数重载里匹配和这个参数个数和类型匹配的重载:

  • 第一次调用,传入了一个数字996,从上到下匹配重载是符合第二个的,所以它的返回值应该是字符串类型,返回"996";
  • 第二次调用,传入了一个字符串"996",从上到下匹配重载是符合第一个的,所以它的返回值应该是数字类型。返回996;
  • 第三次调用,传入了一个布尔类型值false,匹配不到重载,所以会报错;

注意: 函数重载只能用 function 来定义,不能使用接口、类型别名来定义。

文章分类
前端
文章标签