TS官方手册:TypeScript: Handbook - The TypeScript Handbook (typescriptlang.org)

函数类型表达式

使用类似于箭头表达式的形式来描述一个函数的类型。

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

上述代码中,fn: (a:string) => void表示变量fn是一个函数,这个函数有一个参数a,是string类型,且这个函数的返回值类型为void,即没有返回值。

调用签名

在 JS 中,函数是对象,除了可以调用也可以拥有自己的属性。而使用函数类型表达式无法声明这一部分属性的类型。

可以将函数视为一个对象,声明一个类型,其中包含多个属性的类型声明,并使用调用签名来描述函数参数和返回值的类型,取代原先函数类型表达式的写法。

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

在这个例子中,DescribableFunction是一个函数类型,description是这个函数类型实例对象的一个属性名,类型为string。而这个函数的参数列表类型声明为:(someArg: number),返回值类型为boolean

需要注意,在这种写法中,参数列表和返回值类型之间是用:隔开,而函数类型表达式是使用=>

构造签名

搭配构造函数使用,在调用签名的语法前面加上new

type SomeConstructor = {
  new (s: string): SomeObject;
};

泛型函数

如果函数的参数类型与返回值的参数类型存在关联,可以使用泛型:

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

// s是'string'类型
const s = firstElement(["a", "b", "c"]);
// n是'number'类型
const n = firstElement([1, 2, 3]);
// u是undefined类型
const u = firstElement([]);

多类型

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

类型约束

泛型支持函数传入不同的类型,当需要约束时,例如要求传入的参数类型必须包含一个某类型的属性,则可以:

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

使用<Type extends { x : y}>实现,extends表示继承于类型{x:y},表示类型Type应该包含y类型的属性x

需要注意如果函数返回值类型为Type,那么不能返回类型为{x:y},因为Type包含{x:y},而可能存在比{x:y}更多的属性。

指定类型参数

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

// 报错,因为根据第一次参数,Type会被识别为number,但是第二个参数却是string[]类型。
const arr = combine([1, 2, 3], ["hello"]);

如果执意这么设计函数的话,可以考虑使用联合类型:

const arr = combine<string | number>([1, 2, 3], ["hello"]);
使用泛型函数的建议
  1. 尽可能使用类型参数本身,而不去使用类型约束。

    // Good: 返回值类型会被推断为Type
    function firstElement1<Type>(arr: Type[]) {
      return arr[0];
    }
    // Bad: 返回值类型会被推断为any
    function firstElement2<Type extends any[]>(arr: Type) {
      return arr[0];
    }
    
  2. 尽可能少地使用类型参数。

    过多的类型参数会使得函数难以阅读,尽量确保类型参数与多个值相关(例如与函数参数和返回值都有关)再使用。

    // Good: 只用了Type一个类型参数
    function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
      return arr.filter(func);
    }
    // Bad: Func这个类型参数是多余的,只用在了一个函数参数
    function filter2<Type, Func extends (arg: Type) => boolean>(
      arr: Type[],
      func: Func
    ): Type[] {
      return arr.filter(func);
    }
    
  3. 如果类型参数只出现在一个位置,那么这个类型参数很可能不是必要的。

    使用泛型是因为函数中有若干个值的类型存在关联,如果类型参数只出现在一个位置,很可能不是必要的。

    // Bad: Str是不必要的
    function greet<Str extends string>(s: Str) {
      console.log("Hello, " + s);
    }
    // Good
    function greet(s: string) {
        console.log("Hello, " + s);
    }
    

可选参数列表

function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK

:上面的代码中x的类型实际为number|undefined,当不传入该参数的时候就是undefined

如果考虑设置默认值,如下,那么x的类型就会变成number,排除了undefined的情况。

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

:只要一个参数是可选的,那么这个参数就可以被传入undefined

回调函数的可选参数

在设计一个回调函数的函数类型时,不要使用可选参数。

函数重载

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);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
// 报错
const d3 = makeDate(1, 3);

如上述代码,先写两个重载函数签名(overload signatures),然后再写一个函数兼容实现这两个签名,叫做实现签名(implementation signature)。

在调用函数的时候,需要以重载签名为标准,不能以实现签名为标准。也就是说,上面这段代码中的函数makeDate,要么传入1个参数,要么传入3个参数,不能传入2个参数。

  • 从外部无法看见实现的签名。在编写重载函数时,应该始终在函数的实现之上有两个或多个签名。
  • 实现签名与重载签名之间要兼容。
  • 当可以使用联合类型函数参数解决问题时,就不要使用函数重载。

声明this的类型

const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

其它与函数相关的类型

void

void作为函数的返回值类型,表示函数没有返回值。

在 JS 中没有返回值的函数会返回 undefined,但是在 TS 中undefinedvoid是不同的。

当函数返回值声明为void,仍可以在函数体中return内容,但不管返回了什么值,最终接收函数返回值的那个变量都会是void类型。

type voidFunc = ()=>void;
const f: voidFunc = ()=> true;

// 这里value的类型会被推断为void
const value = f();
object

object类型是除了stringnumberbigintbooleansymbolnullundefined的其它类型。

object类型与空对象类型{}不同,与全局类型Object也不同。永远不要使用Object类型,而是使用object

在 JS 中,函数也是对象;在 TS 中,函数也被认为是object类型。

unknown

unknownany非常类似,但是unknown更安全。

因为unknown类型变量的任何操作都是非法的,这迫使大多数操作之前需要对unknown类型变量进行类型的检查。

any类型的值执行操作之前不需要进行任何检查。

function f1(a: any) {
  a.b(); // OK
}

function f2(a: unknown) {
  a.b(); // ERROR: 'a' is of type 'unknown'.
}

unknown类型只能赋值给anyunknown类型。

unknown类型的意义:TS 不允许我们对类型为 unknown 的值执行任意操作。我们必须首先执行某种类型检查以缩小我们正在使用的值的类型范围。

可以使用类型收束(Narrowing)的操作将unknown缩小到具体的类型,再进行后续操作。

never

never通常描述返回值类型,表示永远不返回值。与void不同,使用never意味着函数会抛出一个异常,或者程序会被终止。

function fail(msg: string): never {
  throw new Error(msg);
}

另一种情况下也会出现never,就是当联合类型被不断收窄到空时,就是never

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // 这里x的类型是'never'
  }
}
Function

全局类型Function声明的变量包含了 JS 中函数所拥有的所有属性和方法,例如bindcallapply

Function声明的变量是可执行的,并且返回any

这种函数类型声明方式很不安全,因为返回any,最好使用函数类型表达式声明:()=>void

function doSomething(f: Function) {
  return f(1, 2, 3);
}

不定长参数列表

在 TS 中,不定长参数列表的类型应该被声明为Array<T>T[]或元组类型。

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

spread语法可以展开可迭代对象(例如数组,对象)变成不定长的参数列表。例如push函数可以接收多个参数。

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

但需要注意,TS 认为数组是可变的,数组长度是可变的。

观察下面的案例代码:

const args = [8, 5];
const angle = Math.atan2(...args);

尽管Math.atan2接收两个number类型的参数,而args也刚好是长度为2的number[]类型数组,展开后刚好。

但是这段代码会报错,因为数组是可变的。

一种较为直接的解决方法是使用const

// 视为长度为2的元组
const args = [8, 5] as const;
// 现在不会报错了
const angle = Math.atan2(...args);

参数解构的类型声明

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}

或者使用type简化:

type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}