函数类型
在 JavaScript 中,有两种常见的定义函数的方式:函数声明式和函数定义式。
// 函数声明式(命名函数)
function sum(x, y) {
return x + y;
}
// 函数定义式(匿名函数)
let mySum = function (x, y) {
return x + y;
};
一个函数有形参和返回值,在 TypeScript 中定义函数类型时需要定义参数和返回值的类型。
interface searchFunc {
//(参数1:类型,参数2:类型,....):返回值的类型
(a:string,b:number,c:boolean):boolean
}
const fun1:searchFunc = (a:string,b:number,c:boolean) => {
return false;
};
函数声明式的类型定义
function sum(x: number, y: number): number {
return x + y;
}
//sum(x: number, y: number): number
//表示sum函数要求有两个形参且两个参数都是number类型,函数的返回值是number
- 1.函数参数必须声明数据类型,返回值数据类型可以声明也可以不声明,能够根据返回语句自动推断出返回值类型。
- 2.函数不需要写返回值时,返回值的类型可以是void也可以不写类型定义,而当函数返回值声明了除void和any类型,就必须写return语句并返回一个同类型的值。
//编译报错
function sum(x: number, y: number): number {
// return x + y;
}
//编译报错
function sum(x: number, y: number): void {
return x + y;
}
//编译报错 根据返回语句自动推断出返回值类型是number
function sum(x: number, y: number): string {
return x + y;
}
ps:传递给一个函数的参数个数必须与函数期望的参数个数一致当形参多于要求的或者少于要求的,是不被允许的
function sum(x: number, y: number): number {
return x + y;
}
//编译报错
sum(1, 2, 3);
//编译报错
sum(1);
函数定义式的类型定义
- 变量的类型可以声明为函数类型
- 函数类型属于自定义类型,包含两部分:参数类型和返回值类型
let mySum = function (x: number, y: number): number {
return x + y;
};
这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的mySum变量是通过赋值操作进行类型推论而推断出来的,并没有给mySum这个变量进行类型定义。变量的类型定义需要我们手动给mySum变量添加,如下:
//函数完整的写法
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
ps:不要和ES6中箭头函数弄混淆了
在 TypeScript 的类型定义中,=> 用来表示函数的类型定义,左边是定义函数参数类型,需要用括号括起来,右边是定义函数返回值类型。
在变量冒号后面是定义变量的类型,等号后面是给变量赋值。所以上述代码mySum应该是一个函数,而该函数类型为有两个参数且都是数字类型,返回值也是数字类型的函数。等号后面赋值就应该是一个函数,函数类型需要是两个数字类型的参数,返回值是数字类型的函数。
//声明一个变量并指定类型为自定义的函数类型(变量定义类型)
let myadd:(x:number, y:number)=>number;
//声明一个函数
function add(x: number, y: number): number {
return x + y;
}
//把函数赋值给类型为函数类型的变量
myadd = add;
//变量赋值
myadd = function(x: number, y: number): number {
return x + y;
}
//变量赋值
myadd = (x: number, y: number):number=>{
return x + y;
}
箭头函数定义类型
// js函数基础上定义参数和返回值类型
const isExist = (param: any): boolean => {
return !(param);
}
// 在js函数基础上进行类型推断
const isExist = (param: any) => {
return !(param);
}
// 直接声明函数类型
const isExist: (param: any) => boolean = (param)=> {
return !(param);
}
用接口定义函数的形状
可以使用接口的方式来定义一个函数需要符合的形状。自定函数类型代码往往很长,可以使用接口来封装该类型,之后使用接口来代表该类型。
//函数类型接口
interface addType {
(arg1:number, arg2:number):number
}
//声明一个函数
function add(x: number, y: number): number {
return x + y;
}
//声明一个变量并指定类型为addType类型然后将add函数赋值给myadd1
let myadd1:addType = add;
let myadd2:addType = (x:number, y:number):number=>{
return x+y;
}
采用函数定义式或者接口定义函数的方式时,可以将等号左侧进行类型限制,保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
与接口中的可选属性类似,我们用?表示可选的参数,没有?表示必选参数,传参时必须传参以及数据类型需要一致。
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
//编译通过
let tomcat = buildName('Tom', 'Cat');
//编译通过
let tom = buildName('Tom');
//编译报错
let result2 = buildName("Bob", "Adams", "Sr.");
可选参数必须接在必选参数后面。即可选参数后面不允许再出现必选参数。
function buildName(firstName?: string, lastName: string) {
if (firstName) {
return firstName + ' ' + lastName;
} else {
return lastName;
}
}
//编译报错
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');
参数默认值(默认参数)
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将所有添加了默认值的参数识别为可选参数。与可选参数一样,在调用函数的时候可以省略,同时默认参数可以放在必选参数以及可选参数的后面。
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Jerry', 'Mouse');
//编译通过
let tom = buildName('Tom');
lastName: string = 'Cat'因为设置了默认值,变为可选参数,调用函数不传参也可以编译通过运行时就取默认值
ps:带默认值的可选参数不需要放在必选参数的后面,此时就不受可选参数必须接在必需参数后面的限制。 但是如果带默认值的可选参数出现在必须参数前面,用户必须明确的传入 undefined从而来获得默认值。
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
指定了默认值以后,函数的length属性(length属性表示函数形参的个数),将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
剩余参数
ES6 中,可以使用 ...rest的方式获取函数中的剩余参数,剩余参数只能是最后一个参数。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
//编译报错
function sum(...arg:any[], n1:number,n2:string) {
}
function push(array, ...items) {
items.forEach((item)=>{
array.push(item);
});
}
let a: any[] = [];
push(a, 1, 2, 3);
//items是一个数组,所以我们可以用数组的类型来定义
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
函数重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
简单理解,函数重载是函数名相同,但函数的参数类型或者个数不一样的多个函数。
比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。
// 利用联合类型
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
利用联合类型可以实现需求,但不能够精确的表达,参数和返回值还是以联合类型表达。需要做到输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
//函数重载声明:使用重载定义多个 reverse 的函数类型,可以自动检测函数的参数和返回值类型
function reverse(x: number): number;
function reverse(x: string): string;
//函数实现
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
类型断言
类型断言可以用来手动指定一个变量的类型。
语法:
1.变量 as 类型(建议使用,常用)
2.<类型>变量
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 变量 as 类型
形如<Foo>的语法在 tsx 中表示的是一个 ReactNode,在 ts 中除了表示类型断言之外,也可能是表示一个泛型。所以建议在使用类型断言时,统一使用 变量 as 类型 这样的语法。
类型断言的用途
类型断言的常见用途有以下几种:
将一个联合类型断言为其中一个类型。
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法。而有时候,需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,此时就可以使用类型断言将一个联合类型断言为其中一个类型。
interface Cat {
name:string;
run():void;
}
interface Fish {
name:string;
swim():void;
}
//编译报错,获取 animal.swim 的时候会报错
function isFish(animal:Cat|Fish):boolean{
return typeof animal.swim === "function";
}
//此时可以使用类型断言,将 animal 断言成 Fish,编译通过
function isFish(animal:Cat|Fish):boolean{
return typeof (animal as Fish).swim === "function";
}
但是类型断言只能够欺骗TypeScript 编译器让编译通过,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// 运行时报错:Uncaught TypeError: animal.swim is not a function`
原因是(animal as Fish).swim()这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。
可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。
使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
将任何一个类型断言为any
理想情况下,TypeScript 的类型系统运转良好,每个值的类型都具体而精确。
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
const foo: number = 1;
foo.length = 1;
//编译报错:Property 'length' does not exist on type 'number'.
数字类型的变量 foo 上是没有 length 属性的,故 TypeScript 给出了相应的错误提示。但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
//给window全局对象添加一个属性(声明全局变量)
window.foo = 1;
//编译报错:Property 'foo' does not exist on type 'Window & typeof globalThis'.
上面的例子中,我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。
此时我们可以使用 window as any 临时将 window 断言为 any 类型解决问题:
(window as any).foo = 1;
在 any 类型的变量上,访问任何属性和方法都是允许的。
需要注意,将一个变量断言为
any可以说是解决 TypeScript 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 变量 as any。
将 any 断言为一个具体的类型
在日常的开发中,我们不可避免的需要处理 any 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。遇到 any 类型的变量时,可以选择无视它,任由它滋生更多的 any。
比如,历史遗留的代码中有个 getCacheData,它的返回值是 any:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
// 使用时,将调用了它之后的返回值断言成一个精确的类型
const tom = getCacheData('tom') as Cat;
tom.run();
上面的例子中,我们调用完 getCacheData 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。
类型断言是有限制的,并不是任何一个类型都可以被断言为任何另一个类型。
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A能够被断言为B,只需要A兼容B或B兼容A即可
其实前四种情况都是最后一个的特例。