TypeScript 函数类型

在 TypeScript 中有多种方式去描述函数的签名,例如:函数类型表达式、接口类型。本文介绍只如何用函数类型表达式描述函数的签名,其语法如下:

// 这是一个函数类型,它描述的函数接受两个参数,分别是name和age,name是string类型,age是number类型,这个函数没有返回值
(name: string, age: number) => void // lineA
// 这是一个函数类型,它描述的函数接受一个参数,这个参数是number类型,函数的返回值的类型是 number
(a: number) => number

函数类型表达式语法与 ES2015 的箭头函数语法很相似,但是它不会创建任何函数,只存在于 TypeScript 编译时。从上述代码可以看出,函数的返回值类型放在箭头符号(=>)的后面,参数类型以 :type 的形式放在参数的后面。代码清单1演示了如何使用函数类型表达式。

代码清单1

// 声明一个名为 startHandle 的变量,其数据类型是函数,它没有返回值,接受一个名为fn的参数,fn 的数据类型也是函数
let startHandle: (fn: (a: number, b: number) => void) => void // line A

// 在这里将一个箭头函数赋值给 startHandle
startHandle = (fn: (a: number, b: number) => void) => { // line B
    if (Math.random() < 0.5) {
        fn(1,2)
    } else {
        fn(3,4)
    }
}
function printResult(val1: number,val2: number): void {
    console.log(val1 + val2)
}

startHandle(printResult)

代码清单1中的 line A 和 line B 乍一看不好理解,主要是它太长了,存在冗余的部分,可以使用类型别名解决这个问题。

类型别名

定义类型别名需要用到的关键字是 type,用法如下:

type myFnType = (a: number, b: number) => void

接下来就能在代码中用 myFnType 代替 (a: number, b: number) => void,让代码更加的简洁。修改代码清单1中的代码,得到代码清单2。

代码清单2

type myFnType = (a: number, b: number) => void

let startHandle: (fn: myFnType) => void // line A

startHandle = (fn: myFnType) => { // line B
    if (Math.random() < 0.5) {
        fn(1,2)
    } else {
        fn(3,4)
    }
}

修改之后,代码清单2中的 line A 和line B 比代码清单1中的 line A 和 line B 简洁很多,更加容易理解。

可选参数

代码清单1和代码清单2中的函数类型,它们每一个参数都是必填的,但在某些情况下,我们要让函数参数是可选的,在函数参数的类型注释的前面加一个?就能让这个参数变成可选参数,如代码清单3所示。

代码清单3

// 参数 age 可传也可以不传,如果传了就必须是 number类型
function printDetail(name: string, age?: number): void {
    console.log(`name is ${name}, age is ${age ? age : '??'}`)
}
printDetail('Bella', 23) // 不会有类型错误
printDetail('Bella') // 不会有类型错误
printDetail('Bella', '3') // 有类型错误

默认参数

与可选参数类似,调用函数的时候可以不给默认参数传值,如果不传值,那么该参数会取它的默认值。在函数参数的类型注释的后面加一个= ,再在=的后面跟一个具体的值,就能将这个参数指定为默认参数。修改代码清单3得到代码清单4。

代码清单4

function printDetail(name: string, age: number = 23): void {
    console.log(`name is ${name}, age is ${age}`)
}

在代码清单4中,不需要在 printDetail 的函数体中判断 age 是否存在。如果调用 printDetail 的时候没有给 printDetail 传递第二个参数,那么 age 取值为 23。调用函数的时候如果传递的参数值为 undefined,这相当于没有传参数值。

函数重载

函数重载指的是函数名相同,但参数列表不相同。JavaScript 没有静态类型检查,所以 JavaScript 不支持函数重载,在 TypeScript 中支持函数重载,但是 TypeScript 中的函数重载只存在于它的编译阶段。

在TypeScript中,函数重载的写法如下:

1 function getDate(timestamp: number):number;
2 function getDate(str: string): Date;
3 function getDate(s: number| string): number | Date {
4    if (typeof s === "number") {
5        return s
6   } else {
7        return new Date(s)
8    }
9 }

上述代码中的函数 getDate 有两个重载,一个期望接受一个 number 类型参数,另一个期望接受一个 string 类型的参数。第一行和第二行的函数没有函数体,它们被称为重载签名,第3行到第9行的函数有函数体,它被称为实现签名。

编写重载函数时,重载签名必须位于实现签名的前面,并且实现签名必须与所有的重载签名兼容。代码清单5是一个实现签名与重载签名不兼容的例子。

代码清单5

function getMonth(timestamp: number): number
function getMonth(date: Date): number
function getMonth(d: Date): number {
    if (typeof d === 'number') {
        return new Date(d).getMonth()
    } else {
        return d.getMonth()
    }
}

代码清单5中的 getMonth 有两个重载签名,第一个重载签名接受一个 number 类型的参数,第二个重载签名接受一个 Date 类型的参数,但 getMonth 的实现签名只接受一个Date 类型的参数,这与第一个重载签名不兼容。在代码清单5中,应该将 getMonth 的实现签名中的参数 d 的数据类型改成 Date | string。

调用重载函数时,必须调用某个确定的重载,不能既可能调用第一个重载又可能调用另外的重载,以重载函数 getMonth 为例:

getMonth(2344553444) // 这是没问题的
getMonth(new Date()) // 这是没问题的
getMonth(Math.random() > 0.5 ? 2344553444: new Date()) // 有问题

上述代码第三行不能在编译阶段确定它调用的是哪一个重载,如果非要这么调用,那么你不能使用重载函数。

补充:在 TypeScript 中有一个通用的函数类型,那就是 Function,它表示所有的函数类型。

函数的兼容性

函数类型由参数列表和返回值类型两部分决定,参数名不影响函数的类型,只有当源类型与目标类型的参数列表和返回值都兼容时,源类型才兼容目标类型,但这不意味着目标类型兼容源类型。

当函数的参数列表相同,但返回值类型不同时,函数的兼容性示例代码如下:

let func1: () => string = () => {return '1'}
let func2: () => number = () => {return 1}

// func1 返回 string类型,func2 返回 number类型,它们相互不兼容
func1 = func2 // 不能赋值
func2 = func1 // 不能赋值

let func3: (x: string) => {id: string} = (x: string) => ({id: '1'})
let func4: (x: string) => {id: string, name: string} = (x: string) => ({id: '1', name: 'Bella'})

// func4 的类型兼容 func3 的类型
func3 = func4 // 能赋值
// func3 的类型不兼容func4 的类型
func4 = func3 // 不能赋值

func4 的返回值比 func3 的返回值多了一个 name 字段,由于 TypeScript 是结构化的类型系统,所以 func4 的返回值兼容 func3 的返回值,反之不兼容。

函数的参数列表兼容性比返回值的兼容性更复杂,这是因为参数列表的情况比较多,总体而言分为下面这3种情况:

  1. 函数的参数全部是必填的并且数量一样

这种情况最简单,从左到右依次比较参数的类型,如果每个参数的类型都兼容,那么这两个函数类型彼此兼容,它们可以相互赋值。

  1. 函数的参数全部是必填的,但是数量不一样

如果相同位置上的参数类型不兼容,那么这两个函数类型相互不兼容,它们不能相互赋值;如果相同位置上的参数类型兼容,那么参数较少的函数类型能够赋值给参数较多的函数类型,反之则不能赋值。示例代码如下:

let func1: (a: string, b: number) => void = (a: string, b: number) => {console.log(a, b)}
let func2: (a: string) => void = (a: string) => {console.log(a)}

func1 = func2 // 可以赋值
func2 = func1 // 不能赋值
  1. 函数的可选参数

如果源函数的可选参数在目标函数上同一位置没有找到,那么源函数可以赋值给目标类型,如果找到了但参数类型不一样,那么不能赋值。示例代码如下:

let func1: (a: string, b?: number) => void = (a: string, b?: number) => {console.log(a, b)}
let func2: (a: string) => void = (a: string) => {console.log(a)}


func1 = func2 // 能赋值
func2 = func1 // 能赋值

补充:函数的reset参数,同等于无限个可选参数。重载函数具备多个函数签名,只有当源函数的每一个签名都能在目标函数上找到兼容的签名时,才能将源函数赋给目标函数,否则不能赋值。

原文链接:https://juejin.cn/post/7233765235953680440 作者:何遇er

(0)
上一篇 2023年5月17日 上午10:16
下一篇 2023年5月17日 上午10:26

相关推荐

发表回复

登录后才能评论