[TypeScript | 青训营笔记]

88 阅读8分钟

[TypeScript | 青训营笔记]

这是我参与「第五届青训营」伴学笔记创作活动的第 3 天 🎉

📌 课程主要内容

📌 TypeScript 的优越性

相较于 JavaScript,虽同为弱类型语言,但 TS 的类型检查为静态类型,而 JS 为动态类型,这意味着 TS:

  • 可读性增强
  • 可维护性增强(在编译阶段就可暴露出大部分错误)

以上两点使得 TS 在多人合作的大型项目中,具备更好的稳定性和开发效率

其次,TS 是 JS 的超集,也就是说 TS:

  • 兼容所有 JS 的特性,支持共存
  • 支持渐进式引入和升级

结合以上,TS 凭借它的优越性,如今在使用热度上已然超过 JavaScript,成为了前端领域愈发重要的语言

📌 基本语法

因为 TS 是 JS 的超集,所以 JS 的所有语法在 TS 中都是合法且有效的

因此,下文将不再重复给出关于 JS 的语法,仅将关注点放在 TS 的新语法上

📃 基础数据类型

以下为在 TS 中,如何定义一个基础数据类型的变量:

 // 字符串
 const q: string = 'string';
 // 数字
 const w: number = 1;
 // 布尔值
 const e: boolean = true;
 // null
 const r: null = null;
 // undefined
 const t: undefined = undefined;

相较于 JS 的语法格式,我们可以发现在 TS 中存在以下不同:

  • 声明变量需要指定类型

📃 对象类型

以下为在 TS 中,如何定义一个对象类型的变量:

 interface IBytedancer {
     // 只读属性:约束属性不可在对象初始化外赋值
     readonly jobId: number;
     name: string;
     sex: 'man' | 'woman' | 'other';
     age: number;
     // 可选属性:定义该属性可以不存在
     hobby?: string;
     // 任意属性:约束所有对象属性都必须是该属性的子类型
     [key: string]: any;
 }
 ​
 const bytedancer: IBytedancer = {
     jobId: 2333,
     name: 'xx',
     sex: 'man',
     age: 20,
     hobby: 'play games'
 }

相较于 JS 的语法格式,我们可以发现在 TS 中存在以下不同:

  • 自定义类型需要使用 interface 关键字进行定义(自定义类型名一般以 I 开头,目的是与一般对象、类名作区分)
  • 自定义类型的属性可以添加约束

关于 TS 中自定义类型的约束,可参考 ······

📃 函数类型

以下为在 TS 中,如何定义一个函数:

 function add (x: number, y: number): number {
     return x + y;
 }
 ​
 const mult: (x: number, y: number) => number = (x, y) => x * y;

相较于 JS 的语法格式,我们可以发现在 TS 中存在以下不同:

  • 对于第一种函数定义方式,需要在形参列表后指明返回值类型
  • 对于第二种函数定义方式,是自定义了一个函数类型,并将给类型附给了一个变量

从代码可读性和易读性的角度上看,第二种方式其实并不好(虽然压缩了代码行数),因此可以采用上文中 📃 对象类型 里定义自定义类型的方式:

 interface IMult {
     (x: number, y: number): number;
 }
 ​
 const mult: IMult = (x, y) => x * y;

📃 函数重载

不是很理解课程中的描述

📃 数组类型

以下为在 TS 中,如何定义一个数组:

 // 一般表示
 type IArr1 = number[];
 // 泛型表示
 type IArr2 = Array<string | number | Record<string, number>>;
 // 元组表示
 type IArr3 = [number, number, string, string];
 // 接口表示
 interface IArr4 {
     [key: number]: any;
 }
 ​
 const arr1 = [1, 2, 3, 4, 5, 6];
 const arr2 = [1, 2, '3', '4', {a: 1}];
 const arr3 = [1, 2, '3', '4'];
 const arr4 = ['string', () => null, {}, []];

相较于 JS 的语法格式,我们可以发现在 TS 中存在以下不同:

  • type 关键字用于给复杂的类型取别名
  • Record 是对象类型的一个别名

📃 TS 补充类型

以下为在 TS 中,新补充的数据类型:

 // 空类型:表示无赋值
 type IEmptyFunction = () => void;
 // 任意类型:是所有类型的子类型
 type IAnyType = any;
 // 枚举类型:支持枚举值到枚举名的正、反向映射
 enum EnumExample {
     add = '+',
     mult = '*'
 }
 EnumExample['add' === '+']; // error
 EnumExample['mult' === '*']; // error
 ​
 enum EColor { Mon, Tue, Wed, Thu, Fri, Sat, Sun };
 EColor['Mon'] === 0;
 EColor[0] === 'Mon';
 ​
 // 泛型
 type INumArr = Array<number>;

📃 TS 泛型

让我们考虑一下这样一个需求:

编写一个函数,入参为 val,类型为 number,要求返回一个数组,该数组的元素全部填充为 val

根据需求,易得代码如下:

 function initArr (val: number): number[] {
     return new Array(10).fill(val);
 }

但若将原需求更改为:

编写一个函数,入参为 val,可能为任意类型,要求返回一个数组,该数组的元素全部填充为 val

这样一来,问题就稍稍棘手了些,但好在 TS 中有一个 any 类型,易得代码如下:

 function initArr (val: any): any[] {
     return new Array(10).fill(val);
 }

这样便满足了 val 可能是任意类型的需求

但需要注意的是,使用 any 可能会带来许多隐患,比如下面情况:

 function printArr (arr: number[]) {
     console.log(arr);
 }
 ​
 function plusOne (arr: number[]) {
     arr.forEach((_, idx) => {
         arr[idx] = arr[idx] + 1;
     })
 }
 ​
 function initArr (val: any): any[] {
     return new Array(10).fill(val);
 }
 const arr = initArr('1');
 plusOne(arr);
 printArr(arr);

在以上代码中,arr 的所有元素本应初始化为 number 类型的 1,但因一时疏忽,错误地将所有元素初始化为了 string 类型的 '1'

当在后续调用 plusOne 函数时,arr 的所有元素本应自增 1,即元素值变为 2,但现在不仅没有报错,甚至对每一个元素都拼接了一个 '1',值变成了 '11'

因此,若要完美实现需求,我们应当使用泛型

泛型的特点在于,它不预先指定具体的类型,而是在使用的时候再根据入参指定具体的类型

以下为在 TS 中,如何使用泛型:

 function initArr<T> (val: T): T[] {
     return new Array(10).fill(val);
 }

这样一来,当发生类型不匹配的时候就会报错,这很有利于开发过程中的调试与纠错

📃 类型别名 & 类型断言

 // 通过 type 关键字定义了 IObjArr 的别名类型
 type IObjArr = Array<{
     key: string;
     [objKey: string]: any;
 }>
 function keyBy<T extends IObjArr> (objArr: Array<T>) {
     // 未指定类型时,result 类型为 {}
     const result = objArr.reduce((res, val, key) => {
         res[key] = val;
         return res;
     }, {});
     // 通过 as 关键字,断言 result 类型为正确类型
     return result as Record<string, T>;
 }

📃 字符串/数字 字面量

 // 允许指定字符串/数字必须的固定值
 ​
 // IDomTag 必须为 html、body、div、span 中的其一
 type IDomTag = 'html' | 'body' | 'div' | 'span';
 // IOddNumber 必须为 1、3、5、7、9 中的其一
 type IOddNumber = 1 | 3 | 5 | 7 | 9;

📌 高级类型

📃 联合/交叉类型

如果现在有如下这样一段 JS 代码:

 const bookList = [
     {
         author: 'xiaoming',
         type: 'history',
         range: '2001-2021'
     },
     {
         author: 'xiaoli',
         type: 'story',
         theme: 'love'
     }    
 ]

显然,这是在定义一个用于储存书籍信息列表的变量

但如果将以上 JS 代码转为 TS 语法的格式,理论上就会得到以下代码:

 interface IHistoryBook {
     author: string;
     type: string;
     range: string;
 }
 ​
 interface IStoryBook {
     author: string;
     type: string;
     theme: string;
 }
 ​
 type IBookList = Array<IHistoryBook | IStoryBook>;
 ​
 const bookList: IBookList = [
     {
         author: 'xiaoming',
         type: 'history',
         range: '2001-2021'
     },
     {
         author: 'xiaoli',
         type: 'story',
         theme: 'love'
     }    
 ]

观察以上代码,我们发现,这简直是在自找麻烦,代码量相较之前变得繁琐了不少,而且在定义接口时出现了冗余,比如两种类型都有 string 类型的 author 属性

如果,在我们定义这两种接口的时候,可以放在一起定义,并且重复的属性只需要定义一遍就好了

联合/交叉类型听到了我们的愿望,跳出来给出了以下代码:

 type IBookList = Array<{
     author: string;
 } & ({
     type: 'history';
     range: string;
 } | {
     type: 'story';
     theme: string;
 })>;
 ​
 const bookList: IBookList = [
     {
         author: 'xiaoming',
         type: 'history',
         range: '2001-2021'
     },
     {
         author: 'xiaoli',
         type: 'story',
         theme: 'love'
     }    
 ];

相信不用过多解释,也能明白联合/交叉类型的用法及意义

📃 类型保护与类型守卫

让我们观察以下代码:

 interface IA {
     a: 1,
     a1: 2
 }
 ​
 interface IB {
     b: 1,
     b1: 2
 }
 ​
 function printLog (arg: IA | IB) {
     if (arg.a) {
         console.log(arg.a1);
     } else {
         console.log(arg.b1);
     }
 }

因为 arg 既可能是 IA类型,也有可能是 IB 类型,因此根据是否存在 arg.a 来判断 arg 具体是哪一个类型,这在逻辑上似乎走得通,也没什么毛病

但是由于 TS 内部对类型的保护机制,对于一个联合类型,我们只能操作联合类型中的交集部分,而在以上代码中,IAIB 的交集为空,因此我们在 arg.a 处得到了一个报错

那既然如此,

Q:若要在 TS 实现该代码的原有功能,应如何编写?

A:使用类型守卫

详见以下代码:

 // 类型守卫:定义一个函数,它的返回值是一个类型谓词,生效范围为子作用域
 function isIA (arg: IA | IB): arg is IA {
     // return !!(arg as IA).a;
     return (arg as IA).a !== undefined;
 }
 ​
 function printLog (arg: IA | IB) {
     if (isIA(arg)) {
         console.log(arg.a1);
     } else {
         console.log(arg.b1);
     }
 }

📃 高级类型

📃 函数返回值类型

📌 工程应用

📃 Web

📃 Node