0034. 函数类型
- 1. 🎯 本节内容
- 2. 🫧 评价
- 3. 🔍 「函数类型」章节速览
- 4. 🤔 “函数类型声明”是什么?
- 5. 🤔 如果在声明函数的时候不指定类型信息,而是交给 TS 自行推断类型,会有什么问题?
- 6. 🤔 如何为变量定义函数类型?
- 7. 🤔 TS 对函数参数个数有什么约束?
- 8. 🤔 如何从现有函数获取其类型信息?
- 9. 🤔 函数类型可以采用对象写法吗?
- 10. 🤔 “Function 类型”有什么特点?
- 11. 🤔 “箭头函数的类型”如何声明?
- 12. 🤔 “可选参数”是什么?
- 13. 🤔 “参数默认值”是什么?
- 14. 🤔 如何处理“参数解构”时,函数参数的类型信息?
- 15. 🤔 “rest 参数”的类型如何声明?
- 16. 🤔 “只读参数”如何声明?
- 17. 🤔 返回值是“void 类型”表示什么意思?
- 18. 🤔 never 类型有什么用途?
- 19. 🤔 “函数重载”是什么?
- 20. 🤔 “构造函数的类型”如何声明?
1. 🎯 本节内容
- 函数的类型声明
- 函数重载
- typeof
- Function
- 构造函数
- 参数
- 可选参数
- 参数默认值
- 参数解构
- 剩余参数
- 只读参数
- 返回值
- void 返回值
- never 返回值
2. 🫧 评价
- 函数类型声明,重点关注俩:参数类型声明 + 返回值类型声明,其中参数类型声明非常得灵活,细节也比较多,因为 JS 原生支持的参数传递就非常灵活,并且 TS 在此基础上还添加了一些扩展,比如只读参数、可选参数、等概念。
3. 🔍 「函数类型」章节速览
| 笔记 | 简介 |
|---|---|
| 0091.函数表达式类型 | 本文详细介绍了 TypeScript 中函数表达式类型的定义、语法形式、与函数声明的区别,以及在类型别名和接口中的使用差异,并提供了实践建议和代码示例。 |
| 0092.可选参数与默认参数 | TypeScript 中可选参数(param?: Type)表示参数可省略且类型自动包含 undefined,必须位于必需参数之后;而默认参数(param: Type = value)提供明确的默认值且类型保持为 Type,可在任意位置定义,实践中应优先使用默认参数以提高代码可读性和健壮性。 |
| 0093.剩余参数 | 剩余参数(...args)是 TypeScript 中用于接收任意数量参数并将其收集成数组(或元组)的机制,它必须是函数最后一个且唯一的剩余形式参数,支持泛型与元组类型(TS 4.0+),是替代 arguments 对象、实现灵活可变参函数的类型安全首选方案。 |
| 0094.函数重载 | 本文系统讲解了 TypeScript 函数重载的语法、匹配规则、最佳实践及与联合类型的对比。 |
| 0095.构造函数类型 | 构造函数类型(Constructor Type)是一个可以用 new 关键字实例化的函数类型,也叫类的类型。 |
| 0096.函数的 2 个特殊返回类型 void、never | void 表示函数的返回值毫无意义,你不应该使用它;nerve 表示函数用不结束,不会有返回值。 |
| 0097.函数的 this 参数 | this 参数是 TS 中用于显式声明函数内部 this 类型的特殊伪参数,它作为编译时的类型注解(非实际运行时参数),帮助开发者在编译阶段捕获 this 绑定错误,提高代码的类型安全性和可维护性。 |
| 0098.函数类型的多种声明方式 | 介绍了 3 种声明函数类型的写法及其区别。 |
4. 🤔 “函数类型声明”是什么?
- 函数的类型声明是 TS 中用于约束「函数参数类型」和「函数返回值类型」的类型声明信息。
- 函数类型声明需要在声明函数时指定参数的类型和返回值的类型。
function hello(txt: string): void {
console.log('hello ' + txt)
}2
3
在上面的示例中,函数 hello() 在声明时需要给出参数 txt 的类型(string)以及返回值的类型(void),后者写在参数列表的圆括号后面。void 类型表示没有返回值。
5. 🤔 如果在声明函数的时候不指定类型信息,而是交给 TS 自行推断类型,会有什么问题?
- 函数参数:
- 通常会被推断为
any,因此推荐显式指定参数的类型。
- 通常会被推断为
- 函数返回值:
- 返回值的类型通常可以不写,因为对于函数的返回值而言,TypeScript 推断结果相对来说是比较准的。
- 如果担心 TS 的推断结果不准确,也可以显式指定返回值的类型。
function hello(txt) {
console.log('hello ' + txt)
}
// 推断结果:
// function hello(txt: any): void2
3
4
5
6
上面示例中,由于没有 return 语句,TypeScript 会推断出函数 hello() 没有返回值,也就是 void。
6. 🤔 如何为变量定义函数类型?
当变量被赋值为一个函数时,变量的类型有两种写法:
// 写法一:通过等号右边的函数类型推断
const hello = function (txt: string) {
console.log('hello ' + txt)
}
// 写法二:使用箭头函数形式显式指定类型
const hello: (txt: string) => void = function (txt) {
console.log('hello ' + txt)
}
// 写法二有两个地方需要注意:
// 1. 函数的参数要放在圆括号里面,不放会报错
// 2. 类型里面的参数名(本例是 txt)是必须的2
3
4
5
6
7
8
9
10
11
12
13
如果函数的类型定义很冗长,或者多个函数使用同一种类型,可以使用 type 命令为函数类型定义一个别名。
type MyFunc = (txt: string) => void
// 这样可以简化后续的使用,变量可以指定为这个类型。
const hello: MyFunc = function (txt) {
console.log('hello ' + txt)
}2
3
4
5
6
定义函数类型的时候,不可以省略形参的名字。形参的名字叫什么不重要,但是不能省略。
// type MyFunc = (string) => void // ❌ 错误写法
// 如果写成 (string) => void,TypeScript 会理解成函数有一个名叫 string 的参数
// 而 string 这个关键字在 TS 中已经被用作表示字符串类型了,因此会提示错误:
// Parameter has a name but no type. Did you mean 'arg0: string'?(7051)
// 形参的名字不能省略
type MyFunc = (txt: string) => void // ✅ 1 正确写法
type MyFunc = (abc: string) => void // ✅ 2 正确写法
// 1、2 都是正确写法,从程序功能上来看,它们是等效的,不过 1 更推荐,它的语义更好。2
3
4
5
6
7
8
9
10
7. 🤔 TS 对函数参数个数有什么约束?
实参数量可以小于等于函数类型要求的参数数量。
- 参数少的可以赋值给参数多的
- 参数多的不能赋值给参数少的
函数的实际参数个数可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数:
let myFunc: (a: number, b: number) => number
myFunc = (a: number) => a // ✅ 正确
// myFunc = (a: number, b: number, c: number) => a + b + c // ❌ 报错
// Type '(a: number, b: number, c: number) => number' is not assignable to type '(a: number, b: number) => number'.
// Target signature provides too few arguments. Expected 3 or more, but got 2.(2322)2
3
4
5
6
7
这是因为 JavaScript 函数在声明时往往有多余的参数,实际使用时可以只传入一部分参数。
比如,数组的 forEach() 方法的参数是一个函数,该函数默认有三个参数 (item, index, array) => void,实际上往往只使用第一个参数 (item) => void。因此,TypeScript 允许函数传入的参数不足。
let x = (a: number) => 0
let y = (b: number, s: string) => 0
y = x // ✅ 正确
x = y // ❌ 报错
// Type '(b: number, s: string) => number' is not assignable to type '(a: number) => number'.
// Target signature provides too few arguments. Expected 2 or more, but got 1.(2322)2
3
4
5
6
7
上面示例中,函数 x 只有一个参数,函数 y 有两个参数,x 可以赋值给 y,反过来就不行。
8. 🤔 如何从现有函数获取其类型信息?
如果一个变量要套用另一个函数类型,可以使用 typeof 运算符:
function add(x: number, y: number) {
return x + y
}
const myAdd: typeof add = function (x, y) {
return x + y
}2
3
4
5
6
7
9. 🤔 函数类型可以采用对象写法吗?
函数类型可以采用对象的写法:
// 函数类型的对象写法如下:
// {
// (参数列表): 返回值
// }
// 示例:
let add: {
// (x: number, y: number): number
// 注意返回值用冒号 : 而非箭头 =>
// (x: number, y: number) => number // ❌
// 报错信息:':' expected.(1005)
// 即便你不小心写错了,报错信息也会很友好地提醒你改为冒号。
}
// 等效:
// let add: (x: number, y: number) => number
add = function (x, y) {
return x + y
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意,这种写法的函数参数与返回值之间,间隔符是冒号 :,而不是正常写法的箭头 =>,因为这里采用的是对象类型的写法,对象的属性名与属性值之间使用的是冒号。
这种对象式写法声明函数类型信息 -> 适用于函数本身存在属性的情况。
function f(x: number) {
console.log(x)
}
f.version = '1.0'
let foo: {
(x: number): void
version: string
} = f2
3
4
5
6
7
8
9
10
10. 🤔 “Function 类型”有什么特点?
TypeScript 提供 Function 类型表示函数,特点是:「任何函数都属于这个类型」。
function doSomething(f: Function) {
return f(1, 2, 3)
}2
3
Function 类型的函数可以接受任意数量的参数,每个参数的类型都是 any,返回值的类型也是 any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。
11. 🤔 “箭头函数的类型”如何声明?
箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似:
// 无类型信息
const repeat = (str, times) => str.repeat(times)
// 加上类型信息
const repeat = (str: string, times: number): string => str.repeat(times)
// 参数是函数
function greet(fn: (a: string) => void): void {
fn('world')
}2
3
4
5
6
7
8
9
10
数组 map 方法使用箭头函数的示例:
type Person = { name: string }
const people = ['alice', 'bob', 'jan'].map((name): Person => ({ name }))
// 箭头函数:
// (name): Person => ({ name })
// 等效:
// (name: string): Person => ({ name })2
3
4
5
6
7
8
9
map() 方法的参数是一个箭头函数 (name):Person => ({name})
- 该箭头函数的参数
name的类型string省略了,因为可以从map()的类型定义推断出来; - 箭头函数的返回值类型为
Person;
map() 函数的类型定义:
Array<T>.map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];Array<string>.map<Person>(callbackfn: (value: string, index: number, array: string[]) => Person, thisArg?: any): Person[]
这里的 T 和 U 是泛型,它会根据我们使用的数组类型以及指定的 map 返回值类型来关联。
12. 🤔 “可选参数”是什么?
如果函数的某个参数可以省略,则在参数名后面加问号表示:
function f(x?: number) {
// ...
}
f() // OK
f(10) // OK2
3
4
5
6
参数名带有问号,表示该参数的类型实际上是 显式指定的类型|undefined。
比如,上例的 x 虽然类型声明为 number,但是实际上是 number|undefined:
注意:不要误以为 1 -> x?: number、2 -> x: number | undefined 是完全等价的。
- 1 表示:
x可以传入number,也可以传入undefined,也可以省略; - 2 表示:
x可以传入number,也可以传入undefined,但不能省略;
// 写法 1
function f(x?: number) {
return x
}
f(undefined) // ✅ 正确
// 推断结果:
// function f(x?: number): number | undefined
// 但是,反过来就不成立,类型显式设为 undefined 的参数,就不能省略:
// 写法 2
function f(x: number | undefined) {
return x
}
f() // ❌ 报错
// Expected 1 arguments, but got 0.(2554)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。
这一点很好理解,我们在调用函数的时候,从左往右依次顺序传递参数,先写左边的再写右边的,如果有可以省略的参数,那必然是在最右端。
let myFunc: (a?: number, b: number) => number // ❌ 报错
// 报错位置是在 b 处,报错信息如下:
// A required parameter cannot follow an optional parameter.(1016)2
3
13. 🤔 “参数默认值”是什么?
TypeScript 函数的参数默认值写法,与 JavaScript 一致。
设置了默认值的参数,就是可选的。
function createPoint(x: number = 0, y: number = 0): [number, number] {
return [x, y]
}
// 这里其实可以省略 x 和 y 的类型声明,因为可以从默认值推断出来:
function createPoint(x = 0, y = 0) {
return [x, y]
}
createPoint() // [0, 0]2
3
4
5
6
7
8
9
10
因为设置了默认值的参数,就意味着是可选的,因此可选参数与默认值不允许同时使用。
// ❌ 报错
// Parameter cannot have question mark and initializer.(1015)
function f(x?: number = 0) {
// ...
}2
3
4
5
设有默认值的参数,如果传入 undefined,也会触发默认值。
function f(x = 456) {
return x
}
f(undefined) // 4562
3
4
5
14. 🤔 如何处理“参数解构”时,函数参数的类型信息?
函数参数如果存在变量解构,类型写法如下:
function f([x, y]: [number, number]) {
// ...
}
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c)
}2
3
4
5
6
7
参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些:
type ABC = { a: number; b: number; c: number }
function sum({ a, b, c }: ABC) {
console.log(a + b + c)
}2
3
4
5
15. 🤔 “rest 参数”的类型如何声明?
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同):
// rest 参数为数组
function joinNumbers(...nums: number[]) {
// ...
}
// rest 参数为元组
function f(...args: [boolean, number]) {
// ...
}
// 推断结果:
// function f(args_0: boolean, args_1: number): void2
3
4
5
6
7
8
9
10
11
12
下面是一个 rest 参数的例子:
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x)
}2
3
rest 参数甚至可以嵌套:
function f(...args: [boolean, ...string[]]) {
// ...
}
// 推断结果:
// function f(args_0: boolean, ...args: string[]): void2
3
4
5
6
rest 参数可以与变量解构结合使用:
function repeat(...[str, times]: [string, number]): string {
return str.repeat(times)
}
// 推断结果:
// function repeat(str: string, times: number): string
// 等同于
function repeat(str: string, times: number): string {
return str.repeat(times)
}2
3
4
5
6
7
8
9
10
11
16. 🤔 “只读参数”如何声明?
如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上 readonly 关键字,表示这是只读参数:
function arraySum(arr: readonly number[]) {
// ...
arr[0] = 0 // 报错
// Index signature in type 'readonly number[]' only permits reading.(2542)
}2
3
4
5
注意,在约束函数参数时,readonly 关键字目前只允许用在数组和元组类型的参数前面,如果用在其他类型的参数前面,就会报错。
17. 🤔 返回值是“void 类型”表示什么意思?
// void 类型表示函数没有返回值:
function f(): void {
console.log('hello')
}
// 如果返回其他值,就会报错:
function f(): void {
return 123 // ❌ 报错
// Type 'number' is not assignable to type 'void'.(2322)
}
// void 类型允许返回 undefined 或 null:
function f(): void {
return undefined // ✅ 正确
}
function f(): void {
return null // ✅ 正确
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
需要特别注意的是,如果变量、对象方法、函数参数是一个返回值为 void 类型的函数,那么它接受返回任意值的函数。
type voidFunc = () => void
const f: voidFunc = () => {
return 123
}2
3
4
5
这是因为,这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。
18. 🤔 never 类型有什么用途?
never 类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。
主要有以下两种情况:
// 1. 抛出错误的函数:
function fail(msg: string): never {
throw new Error(msg)
}
// 2. 无限执行的函数:
const sing = function (): never {
while (true) {
console.log('sing')
}
}2
3
4
5
6
7
8
9
10
11
注意,never 类型不同于 void 类型。
- 前者表示函数没有执行结束,不可能有返回值;
- 后者表示函数正常执行结束,但是不返回值,或者说返回
undefined;
// ✅ 正确
function sing1(): void {
console.log('sing')
}
// ❌ 报错
function sing2(): never {
console.log('sing')
}
// A function returning 'never' cannot have a reachable end point.(2534)2
3
4
5
6
7
8
9
10
19. 🤔 “函数重载”是什么?
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]2
TypeScript 对于"函数重载"的类型声明方法是,逐一定义每一种情况的类型声明(当前调用场景下的参数类型声明和返回值类型声明)。
function reverse(str: string): string
function reverse(arr: any[]): any[]
function reverse(stringOrArray: string | any[]): string | any[] {
if (typeof stringOrArray === 'string')
return stringOrArray.split('').reverse().join('')
else return stringOrArray.slice().reverse()
}2
3
4
5
6
7
20. 🤔 “构造函数的类型”如何声明?
- JavaScript 语言使用构造函数,生成对象的实例。
- 构造函数的最大特点,就是必须使用
new命令调用。 - 构造函数的类型写法,就是在参数列表前面加上
new命令。
// 构造函数的使用 - 需要使用 new 命令
const d = new Date()
class Animal {
numLegs: number = 4
}
// 构造函数的类型声明,需要在参数列表前面加上 new 命令
type AnimalConstructor = new () => Animal
function create(c: AnimalConstructor): Animal {
return new c()
}
const a = create(Animal)2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 构造函数的类型写法,也可以采用对象的形式来声明。
type F = {
new (s: string): object
}2
3
- 如果某些函数既是构造函数,又可以当作普通函数使用,比如
Date()。 - 这时,类型声明可以写成下面这样:
type F = {
new (s: string): object
(n?: number): number
}2
3
4