深入TypeScript,函数详解

934 阅读6分钟

1. 前言

上一篇文章,我们初步认识了TypeScript,学习了TypeScript的类型和类型操作。那么这篇文章,我们接着学习TypeScript的函数部分,包括函数的参数类型、返回值类型等。由于之前我并没有TypeScript的开发经验,所以把学习的TypeScript的整个过程记录下来,这将是一个系列。一方面是记录学习的过程,方便随时查阅;另一方面是一点一点分享出来,希望能给想要学习TypeScript的同学一些帮助。

如果想从0开始学习TypeScript的同学,极力推荐我之前的写的文章:

第一篇 -- 邂逅TypeScript,搭建简易ts开发环境

第二篇 -- 初始TypeScript,认识类型和类型操作

第三篇 -- 深入TypeScript,函数详解

学习本文,你将学到:

1. 学会TypeScript的类型断言;
2. 学会可选链的使用;
3. 学会运算符`!!``??`的使用;
4. 学会字面量类型;
5. 学会函数的几种类型;
6. 等等

2. 类型断言

2.1 类型断言(Type Assertions

类型断言就是把比较宽泛的类型转换成一个具体的类型,语法就是使用as关键字;只看概念的话,不容易理解,我们看下具体的使用场景:

场景一:

比如我们获取一个DOM节点,通常会这么写:

const el = document.querySelector('#id');

这个时候我们把鼠标放在el上(文章中所有的demo都是使用vscode编辑器);会看到el的类型是Element,也就是说是个元素类型,有可能是div元素,也有可能是image元素,有可能是任何一种元素;这时候如果想给el设置src属性,编辑器会提示错误:类型“Element”上不存在属性“src”;

// 报错:类型“Element”上不存在属性“src”
el.src = './1.png'

为什么会报错呢?因为这个时候,我们并不能确定el这个元素是image元素,那么如何解决呢?这个时候就会用到类型断言了。

const el = document.querySelector('#id') as HTMLImageElement; 
// 这个时候`el`是`HTMLImageElement`类型,
// `HTMLImageElement`类型是有`src`属性的,所以下边的代码会编译通过
el.src = './1.png' // ok

可以看到,通过类型断言,把Element比较宽泛的类型,转成了HTMLImageElement具体类型;

场景二:

在类中使用类型断言:

class Person {}; 

// Student 继承 Person 类
class Student extends Person {
    study() {};
} 

// sayHello函数接受一个必选参数,类型是`Person`
function sayHello(p: Person) {
    // p.study(); // Error 如果这么写,报错:类型“Person”上不存在属性“study”
    // 所以这里可以使用类型断言来解决这个问题,
    // 把比较宽泛的 Person 类型转成更具体的 Student 类型;
    (p as Student).study(); // ok
} 

const s = new Student(); 

sayHello(s);

sayHello函数中,直接调用p.study()会报错:类型“Person”上不存在属性“study”,因为Person类中确实没study这个方法,但是把Person类型转换为 它的子类Student就可以了。

2.2 非空类型断言

在上下文中,当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

使用场景:

function foo(message: string | null | undefined) {
    // 编译Error: Type 'string | null | undefined' is not assignable to type 'string'
    const msg: string = message;

    // 编译ok
    const ignoreNullAndUndefined: string = message!;
}

3. 可选链操作符 ?. 的使用

可选链操作符?.其实不是TypeScript独有的特性,它是ES11中新增的特性:

  1. 可选链操作符 ?. ;
  2. 它的作用是,当对象的属性不存在时,会短路,直接返回undefined, 如果存在才会继续执行;
  3. 虽然是ECMA提出的,但是可以和TypeScript一起使用;

看下使用场景:

// 定义类型别名 Person
type Person = {
    name: string,
    friend?: {
        name: string,
        car?: {
            color?: string
        }
    }
}

// 声明 info 对象
const info: Person = {
    name: 'hsh',
    friend: {
        name: 'jack'
    }
}

// ErrorObject(info.friend) is possibly 'undefined'.
// 之所以会编译报错,是因为`friend`是一个可选类型,可选类型的话就有可能是`undefined`
const friendName = info.friend.name

// 编译ok,`friend`后加了可选链操作符
const friendNameNew = info.friend?.name

// 编译ok, 因为声明的`info`对象的`friend`对象里没有`car`属性
// 所以到`car`这就返回了`undefined`
const color = info.friend?.car?.color // 输出:undefined

4. 运算符 ?? 和 !! 的使用

4.1 ??的使用

??ES11(ECMAScript2020)新增的操作符,称为空值合并运算符

使用场景:

const msg: string | null = null;

// 如果`msg`存在,取`msg`的值,不存在取`??`后边的值
const content = msg ?? '这是默认值'; 

console.log(content) // 输出:‘这是默认值’

// 效果等价于 const content = msg || '这是默认值';

4.2 !!的使用

!!操作符的作用就是把其他类型转换为 Boolean 类型

使用场景:

const message: string = 'hello';
const flag = !! message;  // 输出:true
// 等价于 Boolean(message)

5. 字面量类型

字面量类型一般结合联合类型使用:

let align: 'left' | 'right' | 'center'

align = 'left' // ok
align = 'right' // ok
align = 'center' // ok

// 不能将类型“"top"”分配给类型“"left" | "right" | "center"”
align = 'top' // error
// 不能将类型“"down"”分配给类型“"left" | "right" | "center"”
align = 'down' // error

可以看出来,能直接将字面量当做是类型,一般配合联合类型使用,可以很好的限制变量的取值范围。

6. 类型守卫

类型守卫其实就是缩小类型范围的,在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为缩小。

常用的类型守卫一般有这四种:typeofinstanceofin操作符和平等缩小(比如:===!==),分别举例说明一下:

6.1 typeof

function foo(value: string | number) {
    if(typeof value === 'string') {
        value.toUpperCase();
        // value++; // Error: 因为`value`这里只能是`string`类型
    } else {
        value++;
        // value.toUpperCase(); // Error: 因为`value`这里只能是`number`类型
    }
}

typeof 类型保护只支持两种形式:typeof v === "typename"typeof v !== typename

6.2 instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

class Teacher {
    teaching() {};
} 

class Student {
    studying() {};
}

function todo(p: Teacher | Student) {
    if(p instanceof Teacher) {
        p.teaching();
    } else {
        p.studying();
    }
}

6.3 in

如果指定的属性在指定的对象或其原型链中,则in 运算符返回true

type Teacher = {
    name: string,
    teach: boolean
}

type Student = {
    name: string,
    study: boolean
}

function printInfo(p: Teacher | Student) {
    console.log(p.name);
    if('teach' in p) {
        console.log('teach:', p.teach);
    }
    if('study' in p) {
        console.log('study:', p.study);
    }
}

6.4 ===!==

type Direction = 'left' | 'right' | 'top' | 'bottom';

function printDirection(direction: Direction) {
    if(direction === 'left') {
        // todo something
    }
}

7. 函数的几种类型

7.1 函数作为参数时的类型

type FooFnType = () => void; // 无参数无返回值函数类型

function bar(fn: FooFnType) {
    fn();
}

function foo() {}

bar(foo); // ok

7.2 定义常量时,编写函数的类型

type AddFnType = (a: number, b: number) => number;

const add: AddFnType = (num1: number, num2: number) => {
    return num1 + num2;
}

console.log(add(10, 20));  // ok

7.3 函数参数的可选类型

注意:可选参数必须写在必选参数后边

function optional(num1: number, num2?: number) {
    console.log(num1)
}

optional(20, 30); // ok
optional(20); // ok
optional(20, undefined); // ok 
optional(); // Error: 应有 1-2 个参数,但获得 0 个。

7.4 函数参数的默认值

// 如果参数有默认值,相当于是个联合类型;
// 比如下边的例子:`num2`的类型其实等价于 number | undefined

function defaultValue(num1: number, num2: number = 30) {}

defaultValue(20); // ok
defaultValue(20, 20); // ok
defaultValue(20, undefined); // ok 
defaultValue(); // Error: 应有 1-2 个参数,但获得 0 个。

7.5 剩余参数 ...args

function rest(num1: number, ...args: number[]) {
    let total = num1;
    for (const i of args) {
        total += i;
    }
    return total;
}

// rest 可以接受任意数量的参数,但至少是一个
const result1 = rest(10);
console.log(result1);

const result2 = rest(10, 20, 30);
console.log(result2); 

8 . 总结

这篇文章主要学习了类型断言、类型守卫、扩展操作符和函数的几种类型,其实非常建议大家去手动的敲一遍文章中的例子,如果只是看一遍的话,很快就会忘掉的。

持续更新中...