入个TypeScript的门(5)——函数与断言

110 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

函数在开发中可以说是经常使用了,但是在TS中,由于类型的校验导致TS的函数不能随心所欲的编写了,我们必须得按规矩来才可以。比如参数是否可选,参数的默认值,剩余参数与返回值等等。

参数可选

我们之前定义的函数都有一个问题,那就是定义函数后,我们使用函数的时候必须传递那么多的参数且类型要正确,不然就会报错:

type Add = (x:number,y:number)=>number

const add:Add = function(x,y) {
    if(x>0 && y>0)  return x+y;
    else return -1;
}

add(1);//报错,应有 2 个参数,但获得 1 个。
add(1,2,3); // 报错,应有 2 个参数,但获得 3 个。

但是在JS中,我们函数的参数是可以不传递的,那就默认为undefined。那么我们TS该咋办呢?

我们可以使用?来实现可选参数:

type Add = (x?:number,y?:number)=>number

const add:Add = function(x,y) {
    if(x>0 && y>0)  return x+y;
    else return -1;
}
add();

不过这样虽然确实可以实现我们参数不传递了,但是又有了新的问题:

image.png

这是因为可选参数就是使用联合类型联合undefined实现的,那么这个参数就可能为number也可能为undefined类型了,这样我们就得使用as来进行断言。

断言

断言是什么?断言就是让一个不确定的事情变得确定。它常见的作用就是将一个大范围类型的变量进行缩小。 比如我们之前说的联合类型的变量,我们只能使用其公共的变量:

type Man = {name:string,play:()=>void}
type Women = {name:string,makeup:()=>boolean}

function getName(person:Man|Women) {
    return person.name;
}

但是有时候我们不确定类型的时候就得访问其中一个类型特有的属性或方法:

...
function isMamOrWoman(person:Man|Women) {
    if(person.makeup) return '女人';
    return '男人'
}
//报错:类型“Man”上不存在属性“makeup”。

这个时候我们就得使用类型断言,告诉编译器我们这个if里面的person就是Women类型

...
function isMamOrWoman(person:Man|Women) {
    if((person as Women).makeup) return '女人';
    return '男人'
}

这样就不报错了。但是得注意我们得慎用类型断言,我们就当我们十分确定这里这个变量就是这个类型时才用,不然它虽然解决了报错问题,但是却可能会引发逻辑错误。比如:

function Fn(x:number|string):number {
    return (x as string).length
}

这样虽然编译通过,但是在真正使用的时候如果x是number类型那么就会造成js报错,中断程序执行。所以类型断言要慎用,且类型断言不等于改变类型,要改变类型得调用JS的方法(Number、String、parseInt等等)。

所以上面那个代码我们就可以使用类型断言来解决了:

type Add = (x?:number,y?:number)=>number

const add:Add = function(x,y) {
  if(x as number>0 && y as number >0)  return x as number+y as number;
  else return -1;
}
add();

当然断言不仅仅只能作用在联合类型上,断言是有一个规定的,只要符合那个规定就可以使用类型断言:

若 A 兼容 B,那么 A 能够被断言为 BB 也能被断言为 A

那么这个兼容是什么意思呢?我们知道,TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。

interface Person {
    name: string;
}
interface Man {
    name: string;
    play(): string;
}

let tom: Man = {
    name: 'Tom',
    play: () => { console.log('run') }
};
let p1: Person = tom;

在上面的例子中,Man 包含了 Person 中的所有属性,除此之外,它还有一个额外的方法 play。TypeScript 并不关心 Man 和 Person 之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 Man extends Person 是等价的:

interface Person {
    name: string;
}
interface Man extends Person{
    play(): string;
}

那么也不难理解为什么 Man 类型的 tom 可以赋值给 Person 类型的 p1 了——就像面向对象编程中我们可以将子类的实例赋值给类型为父类的变量。也就是说:Person 兼容 Man。那么Man和Person就可以相互断言了:

interface Person {
    name: string;
}
interface Man {
    name: string;
    play(): string;
}
function isPerson(p: Person) {
    return (p as Man);
}
function isMan(man: Man) {
    return (man as Person);
}

总之,若 A 兼容 B,那么 A 能够被断言为 BB 也能被断言为 A

同理,若 B 兼容 A,那么 A 能够被断言为 BB 也能被断言为 A

不过这里可能大家有点难以理解,由Person断言为Man是父类向子类断言,是实现了类型大范围的缩小为类型的小范围,但是为啥Man还可以向Person断言呢?这不是有点矛盾吗?这是因为我们的Man是Person的子类,那么Man类型就一定有父类Person类型的属性和方法,这其实比我们大范围向小范围断言更安全。

同理,我们知道在TS中any和unknown的类型范围最大,它兼容我们所有的其他类型。所以我们其他任何类型都可以断言为any和unknown,而any和unknown也可以断言为所有的其他类型。

let a:any;
console.log(a as unknown);
console.log(a as string);
console.log(a as boolean);
console.log(a as number);
console.log(a as object);
console.log((a as []).length);
let b =1;
console.log(b as any);
console.log(b as unknown);
......

基于此我们可以实现不是兼容关系的类型之间的断言:

let a:number = 1;
let b:string = 'aaa';
a = b as any as number

但是这没啥用,官方也不推荐这样用。

参数默认值

我们使用可选参数还有一个注意点,那就是可选参数必须在必选参数后面:

type Add = (x?:number,y:number)=>number  //报错:必选参数不能位于可选参数后

const add:Add = function(x,y) {
    if(x>0 && y>0)  return x+y;
    else return -1;
}

但是我们可以使用参数的默认值解决这个问题:

type Add = (x=undefined,y:number)=>number //报错:只允许在函数或构造函数实现中使用参数初始化表达式

const add:Add = function(x,y) {
    if(x>0 && y>0)  return x+y;
    else return -1;
}

所以我们使用默认参数的时候就只能直接在函数中定义了:(使用接口也不行)

interface Add {
    (x=1,y:number):number   //报错.....
}
------------------------
const add = function(x=undefined,y:number) {
    ...
}

剩余参数

在JS中的剩余参数就是将多余的参数合并为一个数组,这在TS中也是一样的,我们只需要声明这个剩余参数数组的类型就可以了:

function add(x:number,y:number,...rest:number[]):number {
    return x+y+rest.reduce((num,cur)=>num+cur,0);
}
console.log(add(1,2,3,4,5))