# 简介
函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
如果不指定参数类型,TS就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为any。
返回值的类型通常可以不写,因为TypeScript自己会推断出来。有时候出于文档目的,或者为了防止不小心改掉返回值,还是会写返回值的类型。
函数类型里面的参数名与实际参数名,可以不一致。函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即TypeScript允许省略参数。
使用函数表达式声明函数,如果变量被赋值为一个函数,变量的类型有两种写法。
// 写法一
const hello = function (txt:string) {
console.log('hello ' + txt);
}
// 写法二
const hello:
(txt:string) => void
= function (txt) {
console.log('hello ' + txt);
};
2
3
4
5
6
7
8
9
10
如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type命令为函数类型定义一个别名,便于指定给其他变量。
type MyFunc = (txt:string) => void; // 为函数类型定义了一个别名MyFunc
const hello:MyFunc = function (txt) {
console.log('hello ' + txt);
};
2
3
4
如果一个变量要套用另一个函数类型,有一个小技巧,就是使用typeof运算符。任何需要类型的地方,都可以使用typeof运算符从一个值获取类型。
function add(
x:number,
y:number
) {
return x + y;
}
// 函数myAdd()的类型与函数add()是一样的,那么就可以定义成typeof add,因为函数名add本身不是类型,而是一个值,所以要用typeof运算符返回它的类型
const myAdd:typeof add = function (x, y) {
return x + y;
}
2
3
4
5
6
7
8
9
10
函数类型还可以采用对象的写法。注意,这种写法的函数参数与返回值之间,间隔符是冒号:,而不是正常写法的箭头=>,因为这里采用的是对象类型的写法,对象的属性名与属性值之间使用的是冒号。这种写法平时很少用,但是非常合适用在一个场合:函数本身存在属性。
let foo: {
(x:number): void;
version: string // 函数f()本身还有一个属性version
} = f;
2
3
4
函数类型可****使用Interface来声明,定义一个函数需要符合的形状,这种写法就是对象写法的翻版。
interface myfn { // 定义了接口myfn
(a:number, b:number): number; // 这个接口的类型就是一个用对象表示的函数
}
var add:myfn = (a, b) => a + b;
2
3
4
# Function 类型
TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。
Function 类型的值都可以直接执行。Function 类型的函数可以接受任意数量的参数,每个参数的类型都是any,返回值的类型也是any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。
function doSomething(f:Function) {
return f(1, 2, 3);
}
2
3
# 箭头函数
箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。类型声明写在箭头函数的定义里面时,参数的类型写在参数名后面,返回值类型写在参数列表的圆括号后面。注意,类型写在箭头函数的定义里面,与使用箭头函数表示函数类型,写法有所不同。
const repeat = ( //变量repeat被赋值为一个箭头函数,类型声明写在箭头函数的定义里面
str:string,
times:number
):string => str.repeat(times); // 类型写在箭头函数的定义里面-----------------------
function greet(
fn:(a:string) => void //函数greet()的参数fn是一个函数,类型就用箭头函数表示
):void {
fn('world');
} // 使用箭头函数表示函数类型------------------------------------------------------
type Person = { name: string }; // 一个类型别名,代表一个对象,该对象有属性name
const people = ['alice', 'bob', 'jan'].map( //people是数组的map()方法的返回值
(name):Person => ({name}) // 返回一个对象,类型为Person,该对象有一个属性name
);
2
3
4
5
6
7
8
9
10
11
12
13
# 可选参数
如果函数的某个参数可以省略,则在参数名后面加问号表示。参数名带有问号,表示该参数的类型实际上是原始类型|undefined,它有可能为undefined。但反过来就不成立,类型显式设为undefined的参数,就不能省略。
函数的可选参数只能在参数列表的****尾部****,跟在必选参数的后面。如果前部参数有可能为空,这时只能显式注明该参数类型可能为undefined。
函数体内部用到可选参数时,需要判断该参数是否为undefined。
let myFunc: // 参数a可能为空,只能显式注明类型包括undefined,传参时也要显式传入undefined
(
a:number|undefined,
b:number
) => number;
let myFunc: // 第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空
(a:number, b?:number) => number;
myFunc = function (x, y) {
if (y === undefined) {
return x;
}
return x + y;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 参数默认值
TypeScript会将添加了默认值的参数识别为可选参数。如果不传入该参数,或者传入undefined,就会等于默认值。此时就不受「可选参数必须接在必需参数后面」的限制了。
可选参数与默认值不能同时使用。
具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入undefined。
function add(
x:number = 0,
y:number
) {
return x + y;
}
add(1) // 报错
add(undefined, 1) // 正确
2
3
4
5
6
7
8
# 参数解构
函数参数如果存在变量解构,类型写法如下。
function f(
[x, y]: [number, number]
) {
// ...
}
function sum(
{ a, b, c }: {
a: number;
b: number;
c: number
}
) {
console.log(a + b + c);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。
type ABC = { a:number; b:number; c:number };
function sum({ a, b, c }:ABC) {
console.log(a + b + c);
}
2
3
4
# rest 参数
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。rest 参数只能是最后一个参数。
注意,元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数。
rest 参数可以嵌套。
rest 参数可以与变量解构结合使用。
// rest 参数为数组
function multiply(n:number, ...m:number[]) {
return m.map((x) => n * x); // 参数m就是rest类型,它的类型是一个数组
}
// rest 参数为元组
function f(
...args: [boolean, string?]
) {}
// rest 参数甚至可以嵌套。
function f(...args:[boolean, ...string[]]) {
// ...
}
function repeat(
...[str, times]: [string, number]
):string {
return str.repeat(times);
}
// 等同于
function repeat(
str: string,
times: number
):string {
return str.repeat(times);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# readonly 只读参数
如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上readonly关键字,表示这是只读参数。注意,readonly关键字目前只允许用在数组和元组类型的参数前面,如果用在其他类型的参数前面,就会报错。
function arraySum(
arr:readonly number[] //表示为只读参数
) {
// ...
arr[0] = 0; // 报错。如果函数体内部修改这个数组,就会报错
}
2
3
4
5
6
# void 类型
void 类型表示函数没有返回值。void 类型允许返回undefined或null。如果返回其他值,就会报错。如果打开了strictNullChecks编译选项,那么 void 类型只允许返回undefined。如果返回null,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回undefined。
// 打开编译选项 strictNullChecks
function f():void {
return undefined; // 正确
}
function f():void {
return null; // 报错
}
2
3
4
5
6
7
注意,如果变量、对象方法、函数参数是一个返回值为 void 类型的函数,那么并不代表不能赋值为有返回值的函数。相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。这是因为,这时TypeScript认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。
const src = [1, 2, 3];
const ret = [];
src.forEach(el => ret.push(el)); //push()返回插入后数组的长度,对forEach()来说,这个没用
2
3
注意,这种情况仅限于变量、对象方法和函数参数,函数字面量如果声明了返回值是 void 类型,还是不能有返回值。
函数的运行结果如果是抛出错误,也允许将返回值写成void。
除了函数,其他变量声明为void类型没有多大意义,因为这时只能赋值为undefined或者null(假定没有打开strictNullChecks) 。
# never 类型
never类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。
它主要有以下两种情况。
- ****抛出错误的函数。注意,只有抛出错误,才是 never 类型。如果显式用return语句返回一个 Error 对象,返回值就不是 never 类型。由于抛出错误的情况属于never类型或void类型,所以无法从返回值类型中获知,抛出的是哪一种错误。
- ****无限执行的函数。注意,never类型不同于void类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回undefined。
如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是never。如果程序中调用了一个返回值类型为never的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。
注意,有些函数虽然没有return语句,但实际上是省略了return undefined这行语句,真实的返回值是undefined。
一个函数如果某些条件下有正常返回值,另一些条件下抛出错误,这时它的返回值类型可以省略never。函数的返回值无论是什么类型,都可能包含了抛出错误的情况。
function sometimesThrow():number { //返回值其实是number|never
if (Math.random() > 0.5) {
return 100;
}
throw new Error('Something went wrong');
}
const result = sometimesThrow(); //推断为number
2
3
4
5
6
7
8
# 局部类型
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。
function hello(txt:string) {
type message = string; //在函数hello()内部定义的,只能在函数内部使用
let newTxt:message = 'hello ' + txt;
return newTxt;
}
const newTxt:message = hello('world'); // 报错
2
3
4
5
6
7
# 高阶函数
一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)。
(someValue: number) => (multiplier: number) => someValue * multiplier;
# 函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。
function add(
x:number,
y:number
):number;
function add(
x:any[],
y:any[]
):any[];
function add( // 函数本身的类型声明,必须与前面已有的重载声明兼容。
x:number|any[],
y:number|any[]
):number|any[] {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
} else if (Array.isArray(x) && Array.isArray(y)) {
return [...x, ...y];
}
throw new Error('wrong parameters');
} // 参数类型和返回值类型都是number|any[],但不意味着参数类型为number时返回值类型为any[]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
注意,重载的各个类型描述与函数的具体实现之间,不能有其他代码,否则报错。虽然函数的具体实现里面有完整的类型声明,但是函数实际调用的类型,以前面的类型声明为准。
函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突。
重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。
对象的方法也可以使用重载。
函数重载也可以用来精确描述函数参数与返回值之间的对应关系。
type CreateElement = {
(tag:'a'): HTMLAnchorElement;
(tag:'canvas'): HTMLCanvasElement;
(tag:'table'): HTMLTableElement;
(tag:string): HTMLElement;
}
2
3
4
5
6
由于重载是一种比较复杂的类型声明方法,为了降低复杂性,一般来说,如果可以的话,应该优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系。
// 写法一 函数重载
function len(s:string):number;
function len(arr:any[]):number;
function len(x:any):number {
return x.length;
}
// 写法二 联合类型
function len(x:any[]|string):number {
return x.length;
}
2
3
4
5
6
7
8
9
10
函数的合并:可以使用重载定义多个函数类型。
# 构造函数
JavaScript 语言使用构造函数生成对象的实例。类(class)本质上是构造函数。构造函数的最大特点,就是必须使用new命令调用。构造函数的类型写法,一种是在参数列表前面加上new命令,另一种是采用对象形式。
type F = {
new (s:string): object;
};
2
3
某些函数既是构造函数,又可以当作普通函数使用,比如Date()。
type F = {
new (s:string): object;
(n?:number): number;
} // F 既可以当作普通函数执行,也可以当作构造函数使用
2
3
4