本篇整理自 TypeScript Handbook 中 「More on Functions」 章节。
函数类型表达式(Function Type Expressions)
最简单描述一个函数的方式是使用函数类型表达式(function type expression)。
它的写法有点类似于箭头函数:
// 使用函数表达式定义函数
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
// 使用类型别名进行定义
type GreetFunction = (a: string) => void;
语法 (a: string) => void 表示一个函数有一个名为 a ,类型是字符串的参数,这个函数并没有返回任何值。
-
如果一个函数参数的类型并没有明确给出,它会被隐式设置为
any -
注意函数参数的名字是必须的,这种函数类型描述
(string) => void表示的其实是一个函数有一个类型是
any,名为string的参数。
调用签名(Call Signatures)
在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的
如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature)。
type DescribableFunction = {
description: string;
// 注意: 在参数列表和返回的类型之间用的是 : 而不是 =>
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
构造签名 (Construct Signatures)
JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为他们会产生一个新对象
你可以写一个构造签名,方法是在调用签名前面加一个 new 关键词
type SomeConstructor = {
new (s: string): SomeObject;
}
一些对象,比如 Date 对象,可以直接调用,也可以使用 new 操作符调用,而你可以将调用签名和构造签名合并在一起:
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
泛型函数 (Generic Functions)
在 TypeScript 中,泛型就是被用来描述两个值之间的对应关系。我们需要在函数签名里声明一个类型参数 (type parameter):
// Type --- 类型参数 (type parameter)
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
通过给函数添加一个类型参数 Type,并且在两个地方使用它,我们就在函数的输入(即数组)和函数的输出(即返回值)之间创建了一个关联。现在当我们调用它,一个更具体的类型就会被判断出来:
// 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([]);
约束(Constraints)
有时候,我们需要对我们传入的泛型进行约束,这种情况,我们可以使用**约束 (constraint)**对类型参数进行限制。
// 传入的类型参数Type必须存在属性length
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
泛型约束实战(Working with Constrained Values)
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
// 如果约定返回值类型为Type
// 那么函数理应返回与传入参数相同类型的对象,而不仅仅是符合约束的对象
return { length: minimum };
}
}
// 例如: 这里Type的值是number[]
const arr = minimumLength([1, 2, 3], 6);
// 返回值如果是{ length: 3 }
// 那么下边这行代码会报错
console.log(arr.slice(0));
声明类型参数 (Specifying Type Arguments)
TypeScript 通常能自动推断泛型调用中传入的类型参数,但也并不能总是推断出。
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
// error: Type 'string' is not assignable to type 'number'.
const arr = combine([1, 2, 3], ["hello"]);
而如果你执意要这样做,你可以手动指定 Type
const arr = combine<string | number>([1, 2, 3], ["hello"]);
写一个好的泛型函数的一些建议
类型参数下移(Push Type Parameters Down)
TypeScript 不得不用约束的类型来推断 arr[0] 表达式, 而不是等到函数调用的时候再去推断这个元素
所以如果可能的话,直接使用类型参数而不是约束它
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]);
使用更少的类型参数 (Use Fewer Type Parameters)
应该尽可能用更少的类型参数
// good
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
// bad
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
类型参数应该出现两次 (Type Parameters Should Appear Twice)
泛型是被用来描述两个值之间的对应关系的,即函数的输出类型依赖函数的输入类型,或者两个输入的类型以某种形式相互关联。
如果一个类型参数只在函数签名里出现了一次,那它就没有跟任何东西产生关联。
所以 如果一个类型参数仅仅出现在一个地方,强烈建议你重新考虑是否真的需要它
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
应该被修改为
function greet(s: string) {
console.log("Hello, " + s);
}
可选参数(Optional Parameters)
JavaScript 中的函数经常会被传入非固定数量的参数,我们可以使用 ? 表示这个参数是可选的
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
尽管这个参数被声明为 number类型,x 实际上的类型为 number | undefiend,这是因为在 JavaScript 中未指定的函数参数就会被赋值 undefined。
当然你也可以提供有一个参数默认值:
function f(x = 10) {
// ...
}
此时x的类型为number,因为任何 undefined参数都会被替换为10
注意当一个参数是可选的,调用的时候还是可以传入 undefined:
declare function f(x?: number): void;
// All OK
f();
f(10);
f(undefined);
函数重载
一些 JavaScript 函数在调用的时候可以传入不同数量和类型的参数。
// 重载签名 (overlaod signatures) --- 说明一个函数的不同调用方法
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
// 实际调用的函数体 --- 实现签名 (implementation signature)
// 实现签名的调用方式和参数类型 取决于对应的重载签名 --- 实现签名必须和重载签名必须兼容(compatible)
// 实现签名对外界来说是不可见的。当写一个重载函数的时候,你应该总是需要来两个或者更多的签名在实现签名之上。
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);
在函数中声明 this (Declaring this in a Function)
TypeScript 会通过代码流分析函数中的 this 会是什么类型
const user = {
id: 123,
admin: false,
becomeAdmin: function () {
this.admin = true;
},
}
但还是有一些情况需要你明确的告诉 TypeScript this 到底代表的是什么
在 JavaScript 中,this 是保留字,所以不能当做参数使用。但 TypeScript 可以允许你在函数体内声明 this 的类型。
// ts通过传参的形式 指定this的指向 --- 参数传入的this并不会影响运行时
// 如果存在多个参数的时候,this这个特殊的参数必须位于参数列表的第一个
// 如果需要使用this参数,那么该函数就不可以是一个箭头函数
function foo(this: User) {
return this.admin;
}
剩余参数
const args = [8, 5];
// 此时args的类型参数会被推断为number[] --- 参数个数可以为零个或多个
// 但是atan2有且必须接收2个参数
const angle = Math.atan2(...args); // error
修改:
const args = [8, 5] as const;
// 通过 as const 语法将其变为只读元组便可以解决这个问题
const angle = Math.atan2(...args);
参数解构(Parameter Destructuring)
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
函数返回 void
当基于上下文的类型推导(Contextual Typing)推导出返回类型为 void 的时候,并不会强制函数一定不能返回内容。
// 以下代码都是合法的
type voidFunc = () => void;
const f1: voidFunc = () => {
return true;
};
const f2: voidFunc = () => true;
const f3: voidFunc = function () {
return true;
};
但是当一个函数字面量定义返回一个 void 类型,函数是一定不能返回任何东西的
function f2(): void {
// @ts-expect-error
return true;
}
const f3 = function (): void {
// @ts-expect-error
return true;
};