给函数添加类型有多种方式,你可以根据喜欢自由选择,但某些时候只能用特定方式给函数添加类型。

为函数定义类型

函数的类型就是为函数的参数的每个参数添加类型,然后再为函数本身添加返回值类型。ts能根据返回语句自动推断出返回值类型,通常我们可以省略返回值类型。

function add(x: number, y: number): number {
  return x + y;

函数类型表达式

我们还可以用函数类型表达式给函数添加类型,是用箭头函数实现的:

let add: (a: number, b: number) => number;
add = function (a: number, b: number) {
  return a + b;

其中add函数的类型是(a: number, b: number) => number,我们可以看到,该类型注解中包含了函数的参数个数、参数类型和函数的返回类型。只要函数参数类型匹配上就可以了,而不在乎参数名是否一样,也就是说(a: number, b: number) => number这里的类型参数名a,b你可以任意命名为其他名字。

我们上一篇学习了类型别名,我们也可以用type来命名函数的类型:

type TAdd = (a: number, b: number) => number;
let add: TAdd = function (a: number, b: number) {
  return a + b;

有属性的函数

js中函数除了可调用外还可以有属性,但上面三种方式都无法给函数的属性添加类型,这个时候应该用声明对象的方式去写函数的类型:

interface ILib {
  (): boolean;
  version: string;
  doSomething(): void;
function getLib() {
  let lib: ILib = (() => true) as ILib;
  lib.version = '1.0';
  lib.doSomething = () => { };
  return lib;
const lib1 = getLib();
lib1();
lib1.doSomething();

这里我们的类型定义语法略有不同,我们不再使用=>,而是换成了:lib函数上面不仅有属性,也有方法,(): booleanlib函数本体的类型。

我们知道js中函数可以被new关键字用来创建一个新对象,ts中把这称为构造函数,这种函数的类型可以在类型中写明new关键字:

type SomeConstructor = {
  new (s: string): object;
function fn(fn: SomeConstructor) {
  return new fn("hello");

特殊的例子

js中的Date函数,既可以用new关键字调用,也可以直接调用,你可以结合上面提到的两种方式来定义这种函数:

interface CallOrConstruct {
  new (s: string): Date;
  (n?: string): string;

我们经常会写这样的函数:函数的输出类型和函数的输入类型有关联,或者函数的参数间的类型有关联,这个时候就需要用到ts中的泛型去实现了。

我们有这样一个函数getFirstEle:获取数组的第一个元素,这个数组可以是数字数组,也可以是字符串数组等等。

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

不使用泛型的话,只能用any类型,上面这样写是ok的,但函数的返回的类型也是any类型,这样的话,其实并没有完全达到我们想要的效果。

用泛型就能很容易实现我们想要的效果:

function getFirstEle<T>(arr: T[]): T{
  return arr[0];
const name = getFirstEle(['Tom', 'Jack', 'Jim']); //name:string
const age = getFirstEle([32, 27, 23]); //age:number

怎么写好泛型函数

我们从前面讲的内容知道了泛型函数可以做很多有趣的事情,但泛型也要遵循一些准则,下面从三个方面说明怎么写好一个泛型函数。

1.缩小类型参数

类型参数指代的类型范围越小越符合我们期望。

下面这两种写法有类似的作用,但仔细分析我们可以看到两个函数的类型参数不一样,从而推导出的函数返回值类型也不一样。ts解析arr[0]的类型会根据我们的类型约束去解析,而不是等到函数被调用的时候解析,所以firstElement1返回的是类型Type,而firstElement2返回的是any类型,显然firstElement1更符合我们的期望。

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

规则:如果可能,使用类型参数本身而不是去约束它

2.更少的类型参数

推荐在泛型函数中使用更少的类型参数,多的参数反而增加了函数的复杂性,也不利于阅读。

看下面这个例子,filter1函数只需要传Type这一个类型参数,而函数参数arrfunc的类型都受Type的约束。而filter2函数需要传入TypeFunc两个类型参数,仔细分析Func类型是可要可不要的,filter2的这种写法反而还让函数更难阅读。所以我们当然推荐filter1的这种写法。

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);

规则:始终使用尽可能少的类型参数

3.参数类型应该出现两次

类型参数是建立多个值的类型之间的联系的,我们既然声明了类型参数,那它肯定在未来某个地方会用到,而且会出现两次,如果一个类型参数只用到了一次,那么就没必要使用泛型了。

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
greet("world");

比如上面这个函数,完全可以放弃泛型,采用更简单的写法:

function greet(s: string) {
  console.log("Hello, " + s);

规则:如果一个类型参数只出现在一个位置,强烈重新考虑你是否真的需要它

上面我们讲的泛型函数可以实现在任何类型的时候都能满足,但有时我们想让两个值之间有关联,比如用一个值的类型去约束另一个值的类型。

我们现在来写一个方法用来获取两数较长的那个数,那么在比较前,我们一定要确保函数传入的两个参数都有length属性,如果我们用类型去约束函数传入的参数,可以借助extends来实现:

function longest<T extends { length: number }>(a: T, b: T) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
const longerArray = longest([1, 2], [1, 2, 3]); // ok,[1, 2, 3]
const longerString = longest("alice", "bob"); // ok,"alice"
const notOK = longest(10, 100);
//                    ~~类型“number”的参数不能赋给类型“{ length: number; }”的参数。

T extends { length: number }就是用来检查传入的参数是否有length属性,没有的话就不能通过类型检查。我们看到当传入的参数是数组和字符串类型的时候都能通过类型检查,传入number类型的时候就不能通过类型检查了,因为数组和字符串上都有length属性,而数字类型没有length属性。

上面的例子中我们没有指明longest函数返回值类型,上面我们讲过类型推断,在这里ts自然也能自动推断出函数的返回值类型是和我们传入参数的类型 T 一致。

可选参数和默认参数

函数参数是可选的,用?表示:

function toFixed(x: number, n?: number) {
  return x.toFixed(n || 0);
toFixed(11.23); //11
toFixed(11.23, 1); //11.2

可选参数必须放在必须按参数后面。

es6中我们可以给函数配置默认参数,那么toFixed函数也可以用默认参数的方式去实现:

function toFixed(x: number, n = 0) {
  return x.toFixed(n);

可选参数和函数重载帮助函数能接收一系列固定参数,剩余参数 可以帮助函数接收一系列灵活不固定的参数。

剩余参数出现在参数的最后,用...表示:

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

在ts中,剩余参数如果不指定类型,默认是any[],如果指定类型也一定是数组或者元组类型。

ts在推断数组类型时,不会推断数组是不可变的,比如:

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

args推断为number[]类型,而Math.atan2传入的参数是[number, number]类型,大多数情况我们会根据代码具体情况去解决,但通常const是最直接的解决方案:

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

我们可以通过函数重载实现不用方式调用函数。比如我们要写一个生成日期的函数,可以传毫秒一个参数,也可以传年月日三个参数,利用函数重载我们可以这样写:

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); // error
//         ~~~~~~~~~~~~~~没有需要 2 参数的重载,但存在需要 1 或 3 参数的重载。

上面的例子,我们首先编写了两个重载:一个接受一个参数,一个接受三个参数。前两个定义称为重载签名,然后第三个函数才开始具体实现函数的作用,称为实现签名

虽然第三个函数中dy是可选参数,但是因为前两个重载的存在,决定了函数makeDate只能传一个参数或者三个参数,传两个参数就不能通过类型检查。

我们实现函数重载要注意实现签名必须要和重载签名兼容。下面这两种情况是不对的:

场景一:函数参数不兼容

function fn(x: boolean): void;
function fn(x: string): void;
//       ~~~此重载签名与其实现签名不兼容
function fn(x: boolean): void {
  console.log("Hello World")

场景二:函数返回类型不兼容

function fn(x: string): string;
function fn(x: number): boolean;
//       ~~~此重载签名与其实现签名不兼容
function fn(x: string | number) {
  return "Hello World";

正确的写法是,实现签名的参数类型和函数返回值类型都要和重载签名兼容:

function add(...rest: number[]): number;
function add(...rest: string[]): string;
function add(...rest: number[] | string[]): number | string {
  // ...

编写好的重载

和前面讲的泛型一样,我们在写函数重载的时候,也要遵循一些准则,下面举个反例。

我们来编写一个获取数组或者字符串长度的函数:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~没有与此调用匹配的重载。
//   第 1 个重载(共 2 个),“(s: string): number”,出现以下错误。
//     类型“number[] | "hello"”的参数不能赋给类型“string”的参数。
//       不能将类型“number[]”分配给类型“string”。
//   第 2 个重载(共 2 个),“(arr: any[]): number”,出现以下错误。
//     类型“number[] | "hello"”的参数不能赋给类型“any[]”的参数。
//       不能将类型“string”分配给类型“any[]”。

len函数只接受参数为string或者参数为any[]的单一类型的变量,并不接受参数为string | any[]联合类型的的参数

我们不用函数重载,用联合类型来写这个函数是这样的:

function len(x: any[] | string) {
  return x.length;

从这明显能看出不用函数重载更简单清晰。所以在某些情况下,我们能用联合类型实现函数的话,就不要用重载实现了。

ts中还有几种类型经常在函数中出现,为了更好地理解函数,我们需要再了解一下这些类型。

void表示函数没有返回值或者不从返回语句返回任何显示的推断类型。

函数printMsgnoop的返回值类型都是void

function printMsg(msg: string){
  console.log(msg);
function noop(){
  return;

object

object是一种特殊的类型,指的是非原始值类型(stringnumberbigintbooleansymbolnull, or undefined),也不同于空对象类型{}

在js中函数值是对象,那么在ts中也是如此,函数的类型可以理解为都是object。比如你可以这样指定函数类型:

const fn1: object = function noop(){
  return;

unknown

unknown类型表示是未知类型,它有可能是任何类型,这和any类型有点类似。

function f1(a: any) {
  a.toUpperCase(); // OK
function f2(a: unknown) {
  a.toUpperCase();
  //~~~~~~~~~~~~类型“unknown”上不存在属性“toUpperCase”
function f3(a: unknown) {
  if(typeof a === 'string'){
    a.toUpperCase();

f2函数中在使用a的时候不知道a的具体类型,所以不能通过类型检查,f3函数中我们用类型守卫知道astring类型,所以这样用是没有问题的。

从上面例子我们看到unknown类型帮助我们接收了任意类型的变量,但是使用unknown比使用any更安全,因为声明为unknown的变量一定要ts推断出它的具体类型才能用,不然不能通过类型检查。

never

never是指函数永远不可能返回一个值,或者说函数的返回值类型是不可到达的。常见的有两种情况:

  • 函数抛出异常
  • // 函数抛出异常,没有返回值
    function fail(msg: string): never {
      throw new Error(msg);
    
  • 联合类型中没有任何剩余
  • 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类型。

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

    通常我们会避免使用Function类型,因为any返回类型不安全。如果您需要接受任意函数但不打算调用它,则该类型() => void通常更安全。

    More on Functions

    ts中文文档

    分类:
    前端
    标签: