乐趣区

关于前端:TypeScript-官方手册翻译计划四函数

  • 阐明:目前网上没有 TypeScript 最新官网文档的中文翻译,所以有了这么一个翻译打算。因为我也是 TypeScript 的初学者,所以无奈保障翻译百分之百精确,若有谬误,欢送评论区指出;
  • 翻译内容:暂定翻译内容为 TypeScript Handbook,后续有空会补充翻译文档的其它局部;
  • 我的项目地址:TypeScript-Doc-Zh,如果对你有帮忙,能够点一个 star ~

本章节官网文档地址:More on Functions

函数

无论是本地函数,还是从其它模块导入的函数,或者是类上的办法,函数都是任何利用的根本组成部分。它们同样也是值,就和其它值一样,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 示意“某个函数承受名为 string 的参数,类型为 any

当然,咱们也能够应用类型别名为某个函数类型命名:

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

调用签名

在 JavaScript 中,函数能够领有属性,以不便进行调用。然而,TypeScript 的函数类型表达式语法不容许申明属性。如果咱们想要形容某个能够通过属性被调用的货色,那么咱们能够在一个对象类型上编写一个调用签名:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {console.log(fn.description + "returned" + fn(6));
}

留神这个语法和函数类型表达式的语法不太一样。在参数列表和返回值类型之间,它应用的是 : 而不是 =>

结构签名

JavaScript 函数也能够通过 new 运算符进行调用。TypeScript 将这种函数视为结构器,因为它们通常用于创立新对象。你能够在调用签名后面增加 new 关键字,从而编写一个结构签名:

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

有些对象,比方 JavaScript 的 Date 对象,能够间接调用也能够通过 new 调用。你能够在同一类型中任意组合调用签名和结构签名:

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

泛型函数

咱们常常须要编写某个函数,它的输出值类型和输入值类型相关联,或者两个输出值的类型在某种程度上相关联。假如当初有一个函数,它须要返回某个数组中的第一个元素:

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

这个函数能够运行,但可怜的是,它的返回值类型为 any。如果返回值类型和数组类型一样,那就更好了。

在 TypeScript 中,当咱们想要形容两个值之间的对应关系的时候,能够应用泛型。怎么应用呢?只须要在函数签名中申明一个类型参数:

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

通过增加一个类型参数 Type 到函数中,并在两个中央应用这个参数,咱们曾经让函数的输出值(数组)和输入值(返回值)建设了一个分割。当初当咱们再次调用函数的时候,将会失去一个类型更加具体的返回值:

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

推断

留神在下面的例子中,咱们不须要非凡阐明 Type 的类型。因为 TypeScript 能够推断 —— 主动抉择它的类型。

咱们也能够应用多个类型参数。举个例子,能够像上面这样实现一个独立版本的 map

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {return arr.map(func);
}
 
// n 的类型是 string
// parsed 的类型是 number[]
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

留神在这个例子中,TypeScript 能够基于给定的 string 类型数组推断出 Input 类型参数的类型,也能够基于函数表达式的返回值类型(number)推断出 Output 类型参数的类型。

束缚

咱们目前编写的泛型函数实用于所有类型的值。有时候,咱们想要关联两个值,但要求只能对值的某个子集进行操作。这时候,咱们能够应用“束缚”去限度类型参数能够承受的品种。

咱们来编写一个函数,它能够返回两个值长度较长的那个。为了实现这个性能,咱们须要一个 number 类型的 length 属性。这里,咱们通过 extends 子句将类型参数束缚为具备 length 属性的类型:

function longest<Type extends {length: number}>(a: Type, b: Type) {if (a.length >= b.length) {return a;} else {return b;}
}
 
// longerArray 的类型是 number[]
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 的类型是 'alice' | 'bob'
const longerString = longest("alice", "bob");
// 报错!number 类型的值没有 length 属性
const notOK = longest(10, 100);
// Argument of type 'number' is not assignable to parameter of type '{length: number;}'.

在这个例子中,没有什么乏味的事件值得注意。咱们容许 TypeScript 推断 longest 函数返回值的类型。返回值的类型推断也实用于泛型函数。

咱们将 Type 束缚为 {length: number},因而得以拜访 a 参数和 b 参数的 length 属性。如果没有类型束缚,那么咱们是无法访问这个属性的,因为传入的参数可能是其它不具备 length 属性的类型。

longerArraylongerString 的类型是基于函数参数推断进去的。记住,泛型都是将两个或多个值与同一类型相关联!

应用束缚值

上面是应用泛型束缚的时候常见的一个谬误:

function minimumLength<Type extends {length: number}>(
  obj: Type,
  minimum: number
): Type {if (obj.length >= minimum) {return obj;} else {return { length: minimum};
/*
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;}'.
*/  
  }
}

这个函数看起来仿佛没故障 —— Type 被束缚为 {length: number},函数要么返回 Type,要么返回匹配约束条件的值。但问题在于,函数承诺返回一个与传入参数雷同类型的对象,而不是某个匹配约束条件的对象。如果这段代码是非法的,那么你很可能写出上面这样无奈失常运行的代码:

// 'arr' 的值是 {length: 6}
const arr = minimumLength([1, 2, 3], 6);
// 这里会报错,因为 arr 不是数组,没有 slice 办法
console.log(arr.slice(0));

指定类型参数

在一次泛型调用中,TypeScript 通常能够推断出预期的类型参数,但也有例外。举个例子,假如你要编写一个合并两个数组的函数:

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

如果调用该函数的时候传入的两个数组的类型不匹配,那么失常状况下是会抛出谬误的:

const arr = combine([1, 2, 3], ["hello"]);
                             ^^^^^^^
// Type 'string' is not assignable to type 'number'.

不过,如果你本意就是想合并两个类型不匹配的数组,那么你能够手动指定 Type

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

编写良好泛型函数的指南

编写泛型函数很有意思,并且很容易因为应用类型参数而忘乎所以。应用过多类型参数或者在不须要的时候应用约束条件,会导致类型推断很难胜利,对函数的调用者造成困惑。

克制类型参数

上面两种形式编写的函数很类似:

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]);

乍一看这两个函数如同差不多,但 firstElement1 函数要更好。它推断失去的返回值类型是 Type,而 firstElement2 推断失去的返回值类型却是 any,因为 TypeScript 须要应用束缚类型去解析 arr[0] 表达式,而不是在函数调用期间“等着”去解析元素。

规定: 在可能的状况下,请间接应用类型参数,而不是给它设置约束条件

应用更少的类型参数

上面是另一对类似的函数:

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);
}

在第二个函数中,咱们创立了一个类型参数 Func,然而它并没有关联两个值。这是一个危险的信号,因为这意味着调用者传入理论的类型参数的时候,必须毫无理由地手动指定一个额定的类型参数。Func 岂但没有帮上任何忙,反而毁坏了函数的可读性和合理性。

规定: 总是尽可能地应用更少的类型参数

类型参数应该呈现两次

有时候咱们会遗记某个函数可能是不须要应用泛型的:

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

下面的函数能够改写为上面这个更简略的版本:

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

记住,类型参数的目标是关联多个值的类型。如果一个类型参数在函数签名中只应用了一次,那么它其实没有关联任何货色。

规定: 如果一个类型参数在某个中央只呈现了一次,请从新谨慎思考本人是否须要应用类型参数

可选参数

JavaScript 中的函数能够承受的参数数量总是可变的。举个例子,numbertoFixed 办法能够承受一个可选的参数示意数位:

function f(n: number) {console.log(n.toFixed()); // 0 个参数
  console.log(n.toFixed(3)); // 1 个参数
}

在 TypeScript 中,咱们能够应用 ? 标识某个参数是可选的:

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

尽管参数被指定为 number 类型,但它实际上是 number | undefined 类型,因为在 JavaScript 中,没有指定的参数的默认值就是 undefined.

你也能够给参数提供一个默认值:

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

当初,f 函数体中的 x 的类型将为 number,因为任何 undefined 类型的参数都会被替换为 10。留神,当参数可选的时候,调用者总是会传递 undefined 给这个参数,从而简略地模仿一个“失落的”参数。

declare function f(x? : number): void;
// cut
// All ok
f();
f(10);
f(undefined);

回调函数中的可选参数

在你理解了可选参数和函数类型表达式之后,你可能会很容易在编写回调函数的时候犯上面的谬误:

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

很多人之所以编写 index? 以将其作为可选参数,本意是想让上面两种调用都是非法的:

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

这实际上意味着,在调用 callback 的时候可能只传入了一个参数。换句话说,下面的函数定义表明了它的外部实现可能是这样的:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {for (let i = 0; i < arr.length; i++) {
    // 这次我不想要提供 index
    callback(arr[i]);
  }
}

反过来推导,TypeScript 会应用这个外部实现,并抛出一个实际上不可能呈现的谬误:

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

在 JavaScript 中,如果调用函数的时候传入的实参数量多于形参数量,那么多余的参数只会被简略地疏忽掉。TypeScript 同样也是这么解决的。参数数量较少的函数总是能够替换参数数量较多的函数(两个函数的参数类型雷同)。

当为回调函数编写一个函数类型的时候,永远不要应用可选参数,除非你的本意是在调用该函数的时候不传入那个参数。

函数重载

在调用某些 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);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
// No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

在这个例子中,咱们编写了两个重载:一个承受单个参数,另一个承受三个参数。后面的这两个签名称为“重载签名”。

之后,咱们编写了一个带有兼容签名的函数实现。函数有一个“实现签名”,然而这个签名不能被间接调用。即便函数的一个必须参数前面跟着两个可选参数,调用该函数的时候也不能只传入两个参数!

重载签名和实现签名

这是一个常见的让人困惑的中央。人们通常会写出上面的代码,并且不了解为什么会抛出谬误:

function fn(x: string): void;
function fn() {// ...}
// 这里本应该能够不传入任何参数
fn();
^^^^
// Expected 1 arguments, but got 0.

再次重申,用于编写函数体的签名必须不能从内部被“看到”。

实现签名不能从内部被“看到”。当编写重载函数的时候,在函数的代码实现局部的下面,必须始终有两个或者两个以上的签名。

此外,实现签名必须与重载签名兼容。举个例子,上面的写法都是谬误的,因为实现签名没有正确地匹配重载签名:

function fn(x: boolean): void;
// 参数类型不对
function fn(x: string): void;
// This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
function fn(x: string): string;
// 返回值类型不对
function fn(x: number): boolean;
// This overload signature is not compatible with its implementation signature.
function fn(x: string | number) {return "oops";}

编写良好的重载函数

就像泛型一样,在应用重载函数的时候,咱们也须要遵循一些规定。遵循这些规定能够让你的函数更容易被调用、了解和实现。

假如有一个函数能够返回某个字符串或者数组的长度:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {return x.length;}

这个函数很好,咱们在调用的时候能够传入字符串或者数组。然而,咱们无奈传入一个可能是字符串或者数组的值,因为 TypeScript 只能将一个函数调用解析为单个重载:

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
/*
No overload matches this call.
  Overload 1 of 2, '(s: string): number', gave the following error.
    Argument of type 'number[] |"hello"'is not assignable to parameter of type'string'.
      Type 'number[]' is not assignable to type 'string'.
  Overload 2 of 2, '(arr: any[]): number', gave the following error.
    Argument of type 'number[] |"hello"'is not assignable to parameter of type'any[]'.
      Type 'string' is not assignable to type 'any[]'.
*/ 

因为两个重载都有雷同的参数数量和返回类型,所以实际上能够编写一个不应用重载的函数版本:

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

这看起来就好多了!调用者调用该函数的时候能够传入两种参数的任意一种。还有一个额定的益处是,咱们不须要搞清楚正确的实现签名。

在可能的状况下,请始终应用联结类型参数,而不是重载

在函数中申明 this

TypeScript 能够通过代码流剖析推断出函数中的 this 指向。举个例子:

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

TypeScript 晓得 user.becomeAdimin 这个函数对应的 this 是内部对象 user。在大部分状况下这其实就足够了,但也有一些状况,咱们须要更明确地管制 this 指向的对象。JavaScript 标准指出,参数不能取名为 this,因而 TypeScript 就利用了这个“语法空缺”让开发者申明函数体中 this 的类型。

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

在回调格调的 API 中,通常会有另一个对象管制函数何时被调用,因而这种模式很常见。留神,你须要应用 function 一般函数而不是箭头函数:

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.

其它须要理解的类型

在应用函数类型的时候,咱们还须要理解一些额定的类型。它们和之前介绍过的类型一样,也能够在任何中央应用,但和函数上下文的关联尤其严密。

void

void 示意没有返回任何值的函数的返回值。只有某个函数没有 return 语句,或者 return 语句中没有返回任何显式的值,那么函数的返回值类型就会被推断为 void

// 返回值的类型被推断为 void
function noop() {return;}

在 JavaScript 中,没有返回值的函数会隐式返回 undefined。然而,在 TypeScript 中,voidundefined 是不一样的货色。在本章节的最初,咱们会进一步解说相干细节。

voidundefined 是不一样的

object

非凡类型 object 代表的是任意非原始类型的值(stringnumberbigintbooleansymbolnull 或者 undefined)。它和空对象类型 {} 不一样,和全局类型 Object 也不一样。很可能你永远也不会用到 Object

object 不是 Object,请 始终 应用 object

留神,在 JavaScript 中,函数也是对象:它们也有属性,原型链也蕴含 Object.prototype,并且 instanceof Object 返回 true,你还能够给 Object.keys 传递函数等。基于这些理由,TypeScript 中的函数类型被归类为 object

unknown

unknown 类型示意任意值。它和 any 类型很类似,但更加平安,因为对 unknown 类型的值执行任何操作都是不非法的:

function f1(a: any) {a.b(); // OK
}
function f2(a: unknown) {a.b();
  ^  
//Object is of type 'unknown'.
}

在形容函数类型的时候,这很有用,因为你能够形容一个能承受任意值的函数,同时又防止在函数体中呈现任何 any 类型的值。

同样的,你能够形容一个返回 unknown 类型值的函数:

function safeParse(s: string): unknown {return JSON.parse(s);
}
 
// 留神 obj 的类型!const obj = safeParse(someRandomString);

never

有些函数从来不会返回任何值:

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

never 类型示意从来不会被看到的值。当返回值是 never 类型的时候,意味着函数抛出了一个异样,或者终止了程序的执行。

当 TypeScript 确定联结类型中没有其它残余类型的时候,也会用到 never

function fn(x: string | number) {if (typeof x === "string") {// 执行某些操作} else if (typeof x === "number") {// 执行某些操作} else {x; // 类型为 never!}
}

#### Function

全局类型 Function 形容了诸如 bindcallapply 这样的属性,以及其它呈现在 JavaScript 中所有函数值下面的属性。它还有一个特点,就是 Function 类型的值总是能够被调用的,并且都会返回 any

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

这是一个非类型化的函数调用,最好防止这种用法,因为返回值为 any 是不平安的。

如果你须要承受一个任意类型的函数,但不打算调用它,那么应用类型 () => void 会更加平安。

残余参数和开展运算符

残余参数

除了应用可选参数和重载让函数承受固定数量的多个参数以外,咱们也能够定义一个函数,通过残余参数让它承受数量不固定的参数。

残余参数呈现在所有参数前面,应用 ... 语法:

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

在 TypeScript 中,这些参数的类型注解隐式为 any[] 而不是 any,任何给定的类型注解也必须是 Array<T> 或者 T[] 的模式,或者应用元组类型(稍后会学习)。

开展运算符

反过来,咱们能够应用开展语法从数组中提供数量可变的参数。举个例子,数组的 push 办法能够承受任意数量的参数:

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

留神,通常状况下,TypeScript 不会假设数组是不可变的。这可能会导致一些令人诧异的行为:

// 推断的类型是 number[],也就是一个蕴含 0 个或更多数字的数组,而
// 不是一个只有两个数字的数组
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.

针对这种状况,最好的解决方案须要取决于你编写的代码。但总的来说,const 上下文是最间接的解决方案:

// 推断为长度为 2 的元组
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);

针对比拟旧的运行时,应用开展运算符可能须要开启 downlevelIteration。

参数解构

相干浏览:解构赋值

应用参数解构,你能够不便地将单个参数对象解包为函数体中的一个或多个局部变量。在 JavaScript 中,代码看起来是上面这样的:

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);
}

这看起来有点简短,因而你也能够在这里应用类型别名:

// 和下面的成果是一样的
type ABC = {a: number; b: number; c: number};
function sum({a, b, c}: ABC) {console.log(a + b + c);
}

函数的可赋值性

返回值类型为 void

函数的返回值类型为 void 的时候,能够产生一些不寻常的、但在意料之中的行为。

返回值类型为 void 的上下文类型并 不会 强制函数 返回任何货色。换句话说,在实现一个返回值类型为 void 的上下文函数类型(type vf = () => void)的时候,它其实能够返回任意值,只是这些值会被疏忽而已。

因而,类型 () => void 的下述实现都是无效的:

type voidFunc = () => void;
 
const f1: voidFunc = () => {return true;};
 
const f2: voidFunc = () => true;
 
const f3: voidFunc = function () {return true;};

当其中某个函数的返回值被赋值给另一个变量的时候,该变量仍会放弃 void 类型:

const v1 = f1();
 
const v2 = f2();
 
const v3 = f3();

因为有这种行为的存在,所以上面的代码也是无效的。尽管 Array.prototype.forEach 办法冀望承受一个返回值为 void 的函数,而 Array.prototype.push 返回的是数字。

const src = [1, 2, 3];
const dst = [0];
 
src.forEach((el) => dst.push(el));

这里还须要留神一种非凡状况,当某个字面量函数定义的返回值为 void 的时候,该函数 不能 返回任何货色。

function f2(): void {
  // 报错
  return true;
}
 
const f3 = function (): void {
  // 报错
  return true;
};

对于 void 的更多信息,请查阅上面其它的文档:

  • v1 手册
  • v2 手册
  • FAQ –“为什么返回值不是 void 的函数能够赋值给返回值是 void 的函数?”
退出移动版