Skip to main content

Function

函数是应用程序的基本构建块,无论是本地函数、从另一个模块导入的函数,还是类的方法。函数像其他值一样,也是值,TypeScript有许多方式来描述函数的调用方式。TypeScript也允许用户指定函数的输入和输出值的类型,以获得可预期的结果。

隐式any问题

考虑如下示例,此处的参数a和参数b不是被推断成any类型,而是由于ab无法被类型推断,所以退而求其次默认被隐式定义为any类型。

function (a, b): number {
return a + b;
}

返回值

在TypeScript中,返回值一般都是可以推断的。

  1. 通过参数参与返回值运算推断。
  2. 通过函数内部变量参与返回值运算的推断。
// function plus(a: number, b: number): number
function plus (a: number, b: number) {
return a + b
}

如果函数没有返回值,可指明其类型为void:

function plus(a: number, b: number): void {
console.log(a + b);
}

函数类型表达式

描述一个函数最简单的方法是使用函数类型表达式,在语法上和箭头函数很相似:

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

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

greeter(print)

如上,(a: string) => void表示一个函数接受一个参数a,类型为stringvoid表示函数没有返回值。就像函数声明一样,如果参数类型不明确,将被隐式地为any

函数参数名称也是必要的,如下,(string) => void被认为参数名是string,类型隐式地为any

function greeter(fn: (string) => void) { 
// do sth
}

当然,也可以可以使用类型别名来为函数类型命名:

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

调用签名

在JavaScript中,函数除了可被调用外,还可以具有属性。然而,函数类型表达式语法不允许声明属性。如果我们想要描述一个既可调用又具有属性的对象,可使用调用签名来实现。

调用签名也称作函数签名,定义了函数类型,本质就是一个对象:

type DescribableFunction = {
description: string;
(someArg: number): boolean;
}

function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}

function myFunc(someArg: number): boolean {
return someArg > 10;
}

myFunc.description = "test";

doSomething(myFunc);

注意到,调用签名语法与和函数类型表达式稍有不同,使用:来连接参数列表和返回类型,而不是

构造函数签名

JavaScript 函数可以使用new操作符进行调用,称为为构造函数,因为它们通常会创建一个新对象。

在TypeScript中,类表示其实例的类型而非其构造函数:

class Phone {
constructor(
public rom: number,
public ram: number
) {
}
}

// 类型Phone为其实例的类型
function getProduct(Product: Phone){
// Type 'Phone' has no construct signatures.
return new Product()
}

因此,需要单独对构造函数进行类型的定义。

TypeScript中可以通过在调用签名前加上new关键字来编写构造函数签名:

class Phone {
constructor(
public rom: number,
public ram: number
) {
}
}

class Dress {
constructor(
public color: string,
public size: number
) {
}
}

type PhoneConstructor = {
new(rom: number, ram: number): Phone
}

type DressConstructor = {
new(color: string, ram: number): Dress
}

type ProductConstructor = PhoneConstructor | DressConstructor;

function getProduct(Product: ProductConstructor | null): Phone | Dress | null {
switch(Product) {
case Phone:
return new Product(8, 256);
case Dress:
return new Product('red', 175);
default:
return null;
}
}

console.log(getProduct(Phone));
console.log(getProduct(Dress));
console.log(getProduct(null));

像JavaScript的Date对象可以使用或不使用new关键字进行调用。你可以在相同的类型中任意组合调用和构造函数签名:

type CallOrConstruct = {
new(s: string): Date;
(): Date;
}

泛型函数

通常我们需要根据输入的类型推断输出的类型,或者多个输入类型之间进行关联。考虑一个函数,它返回数组中的第一个元素:

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

如上,确实能正常返回第一个元素,但不幸运地是返回类型为any。如果能返回数组元素类型就最好不过了。

在TypeScript中,当我们想要描述两个值之间的对应关系时,可以使用泛型。可以通过在函数签名中声明一个类型参数来实现这一点:

function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}

通过将类型参数Type添加到该函数并在两个位置使用它,我们在函数的输入(数组)和输出(返回值)之间创建了一个链接。现在在调用该函数时,将输出一个更具体的类型:

console.log(firstElement([1, 2, 3]));
console.log(firstElement(['a', 'b', 'c']));
console.log(firstElement([]));

推断

如上,该类型是由TypeScript自动推断出来的。除此之外,还可以使用多个类型参数。例如,map的独立版本将如下所示:

function map<Input, Output>(arr: Input [], func: (arg: Input) => Output):Output[] {
return arr.map(func);
}

map(['1', '2', '3'], (n) => parseInt(n));

类型约束

我们已经编写了一些通用函数,可以适用于任何类型的值。有时我们想要比较两个值,但只能在特定的一些类型上执行操作。在这种情况下,我们可以使用约束来限制类型参数可以接受的类型范围。考虑如下示例:

function longest<T extends { length: number}> (a: T, b: T): T {
if (a.length > b.length) {
return a;
} else {
return b;
}
}

const longerArray = longest([1, 2], [1, 2, 3]);
const longerString = longest('alice', 'bob');

console.log(longerArray); // [1, 2, 2]
console.log(longerString); // alice

如上,由于将T约束为{ length: number },因此可以访问ab参数的.length属性。如果没有类型约束,我们将无法访问这些属性,因为值可能是没有length属性的其他类型。如下:

// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
const longerNumber = longest(10, 100);

使用约束值进行操作

在使用泛型约束时,这是一个常见的错误:

function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
/**
*Type '{ length: number; }' is not assignable to type 'Type'.
'{ length: number; }' is assignable to the constraint of type 'Type',
but 'Type' could be instantiated with a different subtype of
constraint '{ length: number; }'.
*/
return { length: minimum };
}
}

如上,假如说上述代码可正常运行,考虑如下调用:得到的结果与Type并不匹配。

// arr = { length: 6 }
// arr is not compatible with type 'Type'
const arr = minimumLength([1, 2, 3], 6);

指定参数类型

TypeScript通常可以推断泛型调用中的预期类型参数,但不总是能够推断。考虑如下示例:

function combineArray<T>(arr1: T[], arr2: T[]) : T[] {
return arr1.concat(arr2);
}

// Type 'string' is not assignable to type 'number'.
combineArray([1, 2, 3], ['a', 'b', 'c']);

如上,如果使用不匹配的数组调用该函数会产生错误。但如果确实需要这样做,则需要明确T类型:

type numberAndString = number | string;
combineArray<numberAndString>([1, 2, 3], ['a', 'b', 'c']);

编写通用函数的准则

编写通用函数很有趣,但很容易沉迷于类型参数。拥有太多类型参数或在不必要的情况下使用约束条件会降低类型推断的成功率,令调用者感到沮丧。

  • 减少不必要的参数类型
  • 减少类型参数
  • 类型参数至少出现两次

可选参数

JavaScript中的函数通常接受可变数量的参数。例如,数字的toFixed方法可以接受可选的数字数量参数。TypeScript中,可使用?将参数标记为可选的:

function f(n: number, dot?: number) {
console.log(n.toFixed(dot));
}

f(3.1415926)
f(3.1415926, 2);

虽然参数被指定为类型为number,但是当在JavaScript中未指定参数时,该参数实际上将具有类型number | undefined,因为未指定的参数在JavaScript中的值为undefined

在ES6中,可通过?简化可选属性的访问:

function testFn (a: number, b?: number) {
console.log(b?.toString())
}

testFn(1, 2)
testFn(1)

除此之外,也可以给参数提供一个默认值:

function f(n: number, dot = 2) {
console.log(n.toFixed(dot));
}

当函数作为参数时,其参数也是可选的:

type cbFn = <T>(arg: T, index?: number) => void;

function myForEach<T>(arr: T[], callback: cbFn ): void {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}

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

函数重载

在JavaScript中,函数可以使用各种参数计数和类型进行调用,所谓函数重载就是对参数和返回值的多样化设置。

例如Date函数可以接受一个或多个参数进行调用。在TypeScript中,通过编写重载签名,可以指定可以以不同方式调用的函数。考虑如下示例:

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;

function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}

console.log(makeDate(1687399094569));
console.log(makeDate(5, 23, 2023));
// No overload expects 2 arguments,
// but overloads do exist that expect either 1 or 3 arguments.
makeDate(5, 23)

如上,编写了两个重载:一个接受一个参数,另一个接受三个参数,这两个签名称为重载签名。然后编写了一个带有兼容签名的函数实现,称为实现签名。调用时,要么接受一个参数,要么接受三个参数,否则都会编译错误。

重载签名与实现签名

  • 函数的实现签名指的是函数的具体实现,包括函数体内的代码和变量等。
  • 函数的外部(重载)签名是指函数的声明部分,包括函数名、参数列表和返回值等。
  • 在函数重载的情况下,只有函数的外部签名会被TypeScript用来进行类型检查和类型推断。
  • 函数的实现签名是不可见的,TypeScript编译器只会根据外部签名来检查函数的调用是否合法,而不会考虑函数的具体实现。
  • 在编写重载函数时,应该始终在函数的实现之上编写两个或更多的签名。

兼容性

实现签名必须与重载签名兼容,包括参数类型、参数个数、返回值类型等,否则会报错。考虑如下示例:

// This overload signature is not compatible with its implementation signature.
function fn(x: number): void;
function fn(x: string): void;

// error: Argument type isn't right
function fn(x: string): void { }

function fn(x: string | number): void { }

写好重载

如果可能的话,始终优先选择具有联合类型的参数,而不是使用重载。

this

JavaScript中,不同的情况下this会有不同的绑定。JavaScript规范规定参数名不允许为this,因此TypeScript使用这中语法间隙让用户在函数体中声明this的类型。

如果函数需要使用this,那么需要确保this类型声明作为函数的第一个参数,TypeScript会在每个调用函数的地方强制将其内部的this绑定为你所声明的。this关键字在函数签名中被视为保留字而非普通参数。

function fn(this: Date, ...args: number[]) {
console.log(this.getFullYear());
console.log(args);

}

fn.call(new Date(), 1, 2, 3)
fn.apply(new Date(), [1, 2, 3])
const newFn = fn.bind(new Date(), 1, 2, 3);
newFn()

这种模式在回调方式的API中很常见,其中另一个对象通常控制何时调用您的函数。需要注意的是,需要使用函数而不是箭头函数才能获得这种行为。

构造函数

TypeScript不支持ES5类型的构造函数:

function Test(a: number) {
// 'this' implicitly has type 'any'
// because it does not have a type annotation.
this.a = 1
}

基于此,必须手动的指定构造器:

interface ITest {
a: number,
getA(): number
}

interface ITestConstructor {
new (a: number): ITest,
prototype: ITest
}

const Test = (function (this: ITest, a: number) {
this.a = a
} as unknown) as ITestConstructor

Test.prototype.getA = function () {
return this.a
}

const t = new Test(1)
console.log(t.getA());

Function

全局类型Function描述了JavaScript中所有函数值上存在的属性,如 bindcallapply 等。它还具有一个特殊的属性,即类型为Function的值始终可以被调用:

// Function --> (...any) => void
function doSth(f: Function) {
return f(1, 2, 3);
}

function doSth(f: () => void) { }

这是一个未经类型化的函数调用,通常最好避免使用,因为它的参数类型是不安全的any

如果需要接受一个任意函数,但不打算调用它,类型() => void通常更安全,而Function类型的函数通常只在对函数的类型定义没有任何要求的情况下使用。

剩余参数

除了使用可选参数和函数重载定义接受不定数量参数的函数外,还可以使用剩余参数来定义。剩余参数出现在所有其他参数之后,并使用...语法:

function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}

const a = multiply(10, 1, 2, 3, 4);
console.log(a);

在 TypeScript 中,剩余参数的类型注释隐式地为any[],并且所给的任何类型注释必须是Array<T>T[] 的形式,或者是元组类型。

type fn<T> = {
(x:number, ...m: T[]): T[];
}
const multiply: fn<number> = function(x, ...m) {
return m.map((item) => {
return item * x;
})
}

不定参数

反过来,可以使用扩展语法从可迭代对象(例如数组)中提供可变数量的参数:

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

一般情况下,TypeScript不会假设数组是不可变的,这可能会导致一些错误的行为:

// const arr = [1, 2, 3] as const
const arr = <const>[1, 2, 3]
function sum(a: number, b: number, c: number) {
return a + b + c
}

const result = sum(...arr)

一个数组实参展开,这个数组必须是readonly的数组。

参数解构

可以使用参数解构方便地将作为参数提供的对象解包到函数体中的一个或多个局部变量中:

interface User {
name: string;
age: number;
}

function getUserInfo({ name, age }: User) {
return `${name} is ${age} years old`;
}

void

函数的void返回类型可能会产生一些不寻常但是预期的行为。

上下文类型

使用返回类型为void的上下文类型化并不强制函数不返回任何内容,换句话说,使用void返回类型的上下文函数类型在实现时可以返回任何其他值,但将被忽略,结果仍然保留:

type voidFunc = () => void;

const f1: voidFunc = () => {
return true;
};

const f2: voidFunc = () => true;

const f3: voidFunc = function () {
return true;
};

console.log(f1()); // true
console.log(f2()); // true
console.log(f3()); // true

当这些函数中的返回值赋值给另一个变量时,它将保留void类型:

// v1, v2, v3 => void
const v1 = f1();
const v2 = f2();
const v3 = f3();

还有一个特殊情况需要注意,当字面量函数定义具有void返回类型时,该函数不能返回任何内容:

// Type 'boolean' is not assignable to type 'void'.
function f2(): void {
return true;
}

void *

void 0void(0)void *undefined的关系

console.log(void 0 === void(0)); // true
console.log(void 0 === void 100); // true
console.log(void 0 === void 'abc'); // true
console.log(void 0 === undefined); // true

如上可见,void本质上都是undefined,但是undefined却不能被认为是void

undefined

  1. undefined是一种类型。
  2. 类型为undefined的值。
  3. window下的全局属性window.undefined,IE7/8下该属性可写。
  4. 局部作用域下可以以undefined作为变量名。

可见,undefined非常的不安全,因此产生了void 0

类型分配

考虑如下示例为什么能成立:

type TypeTest = (a: number, b: number) => number;

const test: TypeTest = (a, b) => {
return a + b
}

函数的类型声明并没有在函数定义时进行,而是在函数赋值的变量时进行了显式地类型定义。

TypeScript会对一个没有进行注解的函数进行类型推断,如果不能直接推断出其类型,那么就会匹配变量对应的类型。TypeScript会对比函数定义时的类型是否和变量显式定义的类型相匹配。

考虑如下示例:

type TypeTest = (a: string, b: string) => number;

/**
Type '(a: number, b: number) => number' is not assignable to type 'TypeTest'.
Types of parameters 'a' and 'a' are incompatible.
Type 'string' is not assignable to type 'number'.
*/
const test: TypeTest = (a: number, b: number): number => {
return a + b
}

如上,因为类型不兼容导致检查不通过。函数类型定义一定要兼容变量所接受的显式类型定义,一旦类型不兼容就意味着类型不能覆盖。

考虑以下示例:

type TypeTest = (a: number, b: number) => void;

const test: TypeTest = (a: number, b: number): number => {
return a + b
}

变量的函数类型设置的是无返回类型,把有返回类型的定义分配给一个无返回类型的定义,会覆盖返回值。

考虑如下示例:

// Type 'number' is not assignable to type 'void'.
function test(a: number, b: number): void {
return a + b
}

如果类型推断的结果和显式类型定义有冲突的情况下,一定会报错,因为类型定义逻辑行不通。