[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 内部对类型的保护机制,对于一个联合类型,我们只能操作联合类型中的交集部分,而在以上代码中,IA 与 IB 的交集为空,因此我们在 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);
}
}