Skip to content

函数类型

函数(More On Functions)

函数是任何应用的基础组成部分,无论它是局部函数(local functions),还是从其他模块导入的函数,亦或是类中的方法。当然,函数也是值 (values),而且像其他值一样,TypeScript 有很多种方式用来描述它。

1.函数声明

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

typescript
function sum(x: number, y: number): number {
    return x + y;
}

注意,输入多余的(或者少于要求的)参数,是不被允许的:

typescript
function sum(x: number, y: number): number {
    return x + y;
}
sum(1, 2, 3);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

2.函数表达式

最简单描述一个函数的方式是使用函数类型表达式(function type expression它的写法有点类似于箭头函数:

typescript
function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) {
  console.log(s);
}

greeter(printToConsole);

语法

语法 (a: string) => void 表示一个函数有一个名为 a ,类型是字符串的参数,这个函数并没有返回任何值。 如果一个函数参数的类型并没有明确给出,它会被隐式设置为 any。 注意函数参数的名字是必须的,这种函数类型描述 (string) => void,表示的其实是一个函数有一个类型是 any,名为 string 的参数。

我们也可以使用类型别名(type alias)定义一个函数类型:

typescript
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

注意

不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型

3.调用签名(Call Signatures)

在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的。要描述一个带有属性的函数,可以在一个对象类型中写一个调用签名(call signature)。

ts
type DescribableFunction = {
  description: string
  (someArg: number): boolean
}
function doSomething(fn: DescribableFunction) {
  console.log(`${fn.description} returned ${fn(6)}`)
}
ts
interface IFunc {
  (val: number): number
  desc: string
}
const fn: IFunc = (a: number) => {
  return a * 2
}
fn.desc = 'zkp'

:::

注意:

调用签名语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是 : 而不是 =>。

4.构造签名 (Construct Signatures)

JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数 (constructors),因为他们会产生一个新对象。你可以写一个构造签名,方法是在调用签名前面加一个 new 关键词:

typescript
type SomeConstructor = {
  new (s: string): SomeObject; 
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

5.泛型函数 (Generic Functions)

我们经常需要写这种函数,即函数的输出类型依赖函数的输入类型,或者两个输入的类型以某种形式相互关联。如下函数,它返回数组的第一个元素:

ts
function firstElement(arr: any[]) {
  return arr[0];
}
const

注意此时函数返回值的类型是 any,如果能返回第一个元素的具体类型就更好了。 在 TypeScript 中,泛型就是被用来描述两个值之间的对应关系。我们需要在函数签名里声明一个类型参数 :

ts
function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0]
}
// or
type TFn = <Type>(val: Type[]) => Type | undefined
const firstElement: TFn = (val) => {
  return val[0]
}

通过给函数添加一个类型参数 Type,并且在两个地方使用它,我们就在函数的输入(即数组)和函数的输出(即返回值)之间创建了一个关联。现在当我们调用它,一个更具体的类型就会被判断出来:

ts
// s is of type 'string'
const s = firstElement(['a', 'b', 'c'])
// n is of type 'number'
const n = firstElement([1, 2, 3])
// u is of type undefined
const u = firstElement([])

5.1 泛型推断(Inference)

上例中,我们没有明确指定 Type 的类型:(firstElement<string>(["a", "b", "c"])) 类型是被 TypeScript 自动推断出来的。 我们也可以使用多个类型参数,举个例子:

ts
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func)
}
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(['1', '2', '3'], n => Number.parseInt(n))

注意在这个例子中,TypeScript 既可以推断出 Input 的类型 (从传入的 string 数组),又可以根据函数表达式的返回值推断出 Output 的类型。

5.2 泛型约束(Constraints)

有的时候,我们想关联两个值,但只能操作值的一些固定字段,这种情况,我们可以使用约束(constraint)对类型参数进行限制。

如下函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个 number 类型的 length 属性。我们使用 extends 语法来约束函数参数:

ts
function longest<Type extends { length: number }>(a: Type, b: Type) {
  return a.length >= b.length ? a : b
}

// 推断泛型 Type为: 'number[]'
const longerArray = longest([1, 2], [1, 2, 3])

// 推断泛型 Type为: 'alice' | 'bob'
const longerString = longest('alice', 'bob')

// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100) 
// 类型“number”的参数不能赋给类型“{ length: number; }”的参数。ts(2345)

TypeScript 会推断 longest 的返回类型,所以返回值的类型推断在泛型函数里也是适用的。

正是因为我们对 Type 做了 { length: number } 限制,我们才可以被允许获取 a b参数的 .length 属性。没有这个类型约束,我们甚至不能获取这些属性,因为这些值也许是其他类型,并没有 length 属性。

基于传入的参数,longerArraylongerString 中的类型都被推断出来了。 所谓泛型就是用一个相同类型来关联两个或者更多的值。

5.3 泛型约束实战(Working with Constrained Values)

这是一个使用泛型约束常出现的错误:

ts
function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj
  }
  else {
    // 满足{length : number}约束,但并不代表满足 Type 类型
    return { length: minimum } 
    /**
     * 不能将类型“{ length: number; }”分配给类型“Type”。
     * "{ length: number; }" 可赋给 "Type" 类型的约束,
     * 但可以使用约束 "{ length: number; }" 的其他子类型实例化 "Type"
     */
  }
}

警告

这个函数看起来像是没有问题,Type 被 { length: number} 约束,函数返回 Type 或者一个符合约束的值。 而这其中的问题就在于:该函数理应返回与传入**类型参数**相同类型的对象,而不仅仅是符合约束的对象。

5.4 声明类型参数 (Specifying Type Arguments)

TypeScript 通常能自动推断泛型调用中传入的类型参数,但也并不能总是推断出。举个例子,有这样一个合并两个数组的函数:

ts
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2)
}

如果你像下面这样调用函数就会出现错误:

ts
// 不能将类型“string”分配给类型“number”。
const arr = combine([1, 2, 3], ['hello'])

而如果你执意要这样做,你可以手动指定 Type:

ts
const arr = combine<string | number>([1, 2, 3], ['hello']) 

6.可选参数(Optional Parameters)

JavaScript 中的函数经常会被传入非固定数量的参数

我们可以使用 ? 表示这个参数是可选的:

ts
//  type x = number | undefined
function f(x?: number) {
  // ...
}
f() // OK
f(10) // OK

提醒:

尽管这个参数被声明为 number类型,x 实际上的类型为 number | undefiend,这是因为在 JavaScript 中未指定的函数参数就会被赋值 undefined。

你当然也可以提供有一个参数默认值

ts
function f(x = 10) {
  // ...
}

信息

现在在 f 函数体内,x 的类型为 number,因为任何 undefined 参数都会被替换为 10。注意当一个参数是可选的,调用的时候还是可以传入 undefined:

ts
declare function f(x?: number): void
// cut
// All OK
f()
f(10)
f(undefined)

6.1 回调中的可选参数(Optional Parameters in Callbacks)

在你学习过可选参数和函数类型表达式后,你很容易在包含了回调函数的函数中,犯下面这种错误:

ts
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++)
    callback(arr[i], i)
}

将 index?作为一个可选参数,本意是希望下面这些调用是合法的:

ts
myForEach([1, 2, 3], a => console.log(a))
myForEach([1, 2, 3], (a, i) => console.log(a, i))

但 TypeScript 并不会这样认为,TypeScript 认为想表达的是回调函数可能只会被传入一个参数,换句话说,myForEach 函数也可能是这样的:

ts
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    // I don't feel like providing the index today
    callback(arr[i])
  }
}

TypeScript 会按照这个意思理解并报错,尽管实际上这个错误并无可能:

ts
myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed())
  // Object is possibly 'undefined'.
})

那如何修改呢?不设置为可选参数其实就可以:

ts
function myForEach(arr: any[], callback: (arg: any, index: number) => void) {
  for (let i = 0; i < arr.length; i++)
    callback(arr[i], i)
}
myForEach([1, 2, 3], (a, i) => {
  console.log(a)
})

在 JavaScript 中,如果你调用一个函数的时候,传入了比需要更多的参数,额外的参数就会被忽略。TypeScript 也是同样的做法。

当你写一个回调函数的类型时,不要写一个可选参数, 除非你真的打算调用函数的时候不传入实参

7. 函数重载(Function Overloads)

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'

利用联合类型,我们可以这么实现:

typescript
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。

Preview

这时,我们可以使用重载定义多个 reverse 的函数类型:

typescript
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}
Preview

上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

注意:

TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

8. 在函数中声明 this (Declaring this in a Function)

TypeScript 会通过代码流分析函数中的 this 会是什么类型,举个例子:

ts
const user = {
  id: 123,
  admin: false,
  becomeAdmin() {
    this.admin = true
  },
}

TypeScript 能够理解函数 user.becomeAdmin 中的 this 指向的是外层的对象 user,这已经可以应付很多情况了,但还是有一些情况需要你明确的告诉 TypeScript this 到底代表的是什么。 在 JavaScript 中,this 是保留字,所以不能当做参数使用。但 TypeScript 可以允许你在函数体内声明 this 的类型。

ts
interface DB {
  filterUsers: (filter: (this: User) => boolean) => User[]
}
const db = getDB()
const admins = db.filterUsers(function (this: User) {
  return this.admin
})

这个写法有点类似于回调风格的 API。注意你需要使用 function 的形式而不能使用箭头函数:

ts
interface DB {
  filterUsers: (filter: (this: User) => boolean) => User[]
}
const db = getDB()
const admins = db.filterUsers(() => this.admin)
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.

9 .剩余参数(Rest Parameters and Arguments)

parameters 与 arguments arguments 和 parameters 都可以表示函数的参数,由于本节内容做了具体的区分,所以我们定义 parameters 表示我们定义函数时设置的名字即形参,arguments 表示我们实际传入函数的参数即实参。

1. 剩余参数(Rest Parameters)

除了用可选参数、重载能让函数接收不同数量的函数参数,我们也可以通过使用剩余参数语法(rest parameters),定义一个可以传入数量不受限制的函数参数的函数: 剩余参数必须放在所有参数的最后面,并使用 ... 语法:

ts
function multiply(n: number, ...m: number[]) {
  return m.map(x => n * x)
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4)

在 TypeScript 中,剩余参数的类型会被隐式设置为 any[] 而不是 any,如果你要设置具体的类型,必须是 Array 或者 T[]的形式,再或者就是元组类型(tuple type)。

2.剩余参数(Rest Arguments)

我们可以借助一个使用 ... 语法的数组,为函数提供不定数量的实参。举个例子,数组的 push 方法就可以接受任何数量的实参:

ts
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
arr1.push(...arr2)

注意一般情况下,TypeScript 并不会假定数组是不变的(immutable),这会导致一些意外的行为:

ts
// 类型被推断为 number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5]
const angle = Math.atan2(...args)
// A spread argument must either have a tuple type or be passed to a rest parameter.

修复这个问题需要你写一点代码,通常来说, 使用 as const 是最直接有效的解决方法:

ts
// Inferred as 2-length tuple
const args = [8, 5] as const
// OK
const angle = Math.atan2(...args)

通过 as const 语法将其变为只读元组便可以解决这个问题。 注意当你要运行在比较老的环境时,使用剩余参数语法也许需要你开启 downlevelIteration ,将代码转换为旧版本的 JavaScript。

10 .参数解构(Parameter Destructuring)

你可以使用参数解构方便的将作为参数提供的对象解构为函数体内一个或者多个局部变量,在 JavaScript 中,是这样的:

ts
function sum({ a, b, c }) {
  console.log(a + b + c)
}
sum({ a: 10, b: 3, c: 9 })
// 在解构语法后,要写上对象的类型注解:
function sum({ a, b, c }: { a: number, b: number, c: number }) {
  console.log(a + b + c)
}

这个看起来有些繁琐,你也可以这样写:

ts
// 跟上面是有一样的
type ABC = { a: number, b: number, c: number }
function sum({ a, b, c }: ABC) {
  console.log(a + b + c)
}