【你应该掌握的】深入浅出typescript
介绍
TypeScript是一种由微软开发的开源、跨平台的编程语言。它是JavaScript的超集,最终会被编译为JavaScript代码。
TypeScript可以解决JavaScript弱类型和没有命名空间,难以模块化的问题,同时也增强了代码的可读性,在团队协作和大型项目中体现出更大的优势。
本文将从“基础用法、高阶用法、模块、react项目实践中的应用”四个方向展开文章,方便大家理解,都会备注列子、codesandbox上的远程代码。
基础用法
1.变量类型
代码示例 src/components/baseVar.tsx
- TypeScript提供与JavaScript几乎相同的数据类型(布尔值boolean,字符串string,数字number,数组[],元组),此外还提供了枚举enum类型。
- 当不确定变量的数据类型或者数据类型会在代码运行过程中变化时,使用any来标记这些变量,使它们通过编译阶段的检查。
- object表示原始类型以外的类型,即number,string,boolean,symbol,null,undefined以外的类型。
- void用于表示没有任何类型,通常用于方法没有返回值时。void类型的变量只能赋值undefined或者null。
- null和undefined各自的类型分别为null和undefined,并没有太大用处。
- never表示永不存在的类型或方法无法达到终点,当代码中抛出异常或者while(true)时可能会用到。
- symbol(自ECMAScript 2015起的新的原生类型),不可改变且唯一,通过Symbol函数构造。TypeScript中内置了一些symbols表示语言内部行为。
- 类型断言: as / <> / !
当TypeScript无法推断出对象类型但开发者可以确定时,可以通过类型断言来显式表达变量类型。
在实际使用过程中,原始类型和any以及类型断言较为常用,object,void,null,undefined,never,symbol不经常使用。
2.接口
代码示例 src/components/interface.tsx
相较于基础类型而言,接口用于更具体地声明更加复杂的对象结构,关键字为interface。
声明方法
方法体内的入参名称不要求与接口声明中完全一致。
声明变量
变量声明中:
- readonly关键字表示属性只读,不可修改。
- ?表示可选属性,即可以不传该字段,可选参数在必传参数后面。
- [propName: string]:any可以匹配到除前面已声明属性外的所有字符串名称的属性,如:sex: boolean, address: any, email: string等,可用于不确定入参中是否还包含其他属性时。
方法的属性排序不要求与接口声明完全一致。
设置默认值
默认值属于可选参数的一种,但默认值不要求在必选参数之后。
当默认值在可选参数前时,需要传入undefined来获取默认值。
剩余参数
当方法中传参数量不一定或需要批量操作入参时,可以通过剩余参数来操作多个参数。
3.简单的高级类型
代码示例 src/components/types.tsx
交叉类型(&)
交叉类型是将多个类型合并为一个类型,包含所有所需类型的所有属性。
联合类型(|)
联合类型表示一个值可以是几个类型之一。
如果一个值是联合类型,我们只能访问所有类型的共有成员。
字符串/数字字面量类型
字符串字面量类型允许指定字符串类型的固定值。
数字字面量类型允许指定数字类型的固定值。
4.迭代器
代码示例 src/components/for.tsx
因为一些内置的类型Map,Array等实现了各自的Symbol.iterator,因此他们都是可迭代的。
(Symbol.iterator方法,被for-of语句调用。返回对象的默认迭代器)
以下语句可用于遍历可迭代对象。
for…of…
(当生成目标为ES5或ES3,迭代器只允许在Array类型上使用。 在非数组值上使用 for..of语句会得到一个错误,就算这些非数组值已经实现了Symbol.iterator属性。)
for…in…
高阶用法
1.泛型
代码示例 src/components/identity.tsx
泛型,指不预先确定的数据类型,在使用时才去确定。这里的使用时,不是指代码运行时。
泛型的本质是将类型参数化,这种参数化的类型可以使用在类、接口及方法中成为泛型类、泛型接口、泛型方法。
泛型是用于创建可复用代码组件的重要工具,使得代码片段可以被多种数据类型使用。
考虑当一个方法有多种可能类型的入参,或者一个变量有多种可能类型的赋值时,通过联合类型列出所有可能的类型有时候是不现实或者繁杂的。当然,我们也可以将其定义为any类型来通过编译阶段的检查,但带来的问题是类型准确性的丢失,调试时也无法看到完整的参数信息。正是因此,TypeScript并不推荐使用any,因为使用any与不引入TypeScript是没有太大区别的。
泛型可用于解决这类问题。
书写泛型接口或方法或类时,我们通过一个类型变量来捕获参数的类型,显示在尖括号<>中,命名随意,通常以大写字母代表。
在实际调用该方法时,给类型变量赋予实际的类型,如string,array,number,boolean等;如不赋值具体类型,则TypeScript通过类型推断来检查代码。
在示例代码中,func1,func2,func3返回与入参类型一致的出参。< T > < U >捕获入参的类型,并用于规范方法的出参。
泛型约束
有时,我们对方法的入参虽然不限制类型,但仍然期望他具备某个属性或者满足某些限制,此时我们可以使用extends关键字继承接口来给泛型增加约束。
示例中func4与func5的区别在于,func5中T extends Leng。当给T增加了约束必须具备length属性时,arg就被限制了类型不可以是number,并且arg.length也不存在undefined的情况。
2.复杂的高级类型
区分类型
代码示例 src/components/types.tsx
当出现联合类型(Person | Worker)时,有时我们需要调用非共有属性,此时需要进一步确认当前变量究竟属于哪个类型。TypeScript提供了一些方法进行类型区分。
- typeof / instanceof
typeof与instanceof可以被TypeScript识别为原始类型的类型保护,我们可以借此进行原始类型的区分。 - 用户自定义的类型保护
当需要对非原始类型进行判断时,用户可以自定义一个类型保护函数,返回值是一个类型谓词,类型谓词形式为paramName is TypeName。
该示例中isWorker就是一个类型保护函数,用于判断入参是否是Worker类型,类型谓词是p is Worker。
- 类型断言
如果开发人员确定当前变量在上下文是什么类型,也可以通过类型断言as/<>显式表明当前变量类型来通过TypeScript的检查。
在实际开发需求中,我们也经常会遇到某些参数或变量可能为null/undefined的情况,此时调用某些方法就会产生报错。
我们可以使用类型断言标记!后缀手动去除null和undefined。
类型别名(Type)
代码示例 src/components/types.tsx
类型别名会给类型(包括原始类型)起一个新的名字。给原始类型取别名可以增强代码可读性,类似于文档,但较少使用。类型别名更多地使用于非原始类型的情况。
类型别名(type)与接口(interface)非常相似,但存在以下几点区别:
- 接口创建了新的名字,但类型实际上没有
- 接口可以被extends和implements,但类型不支持
- 如果需要使用联合类型或交叉类型,这时会选择类型别名
可辨识联合
代码示例 src/components/union.tsx
可辨识联合也称标签联合或代数数据类型。它是将单例类型,联合类型,类型保护和类型别名合并起来创建的,具有以下三个特征:
- 具有普通的单例类型属性 - 是该数据类型可辨识的特征
- 一个数据类型包含了多种类型的联合
- 该属性上的类型保护 (可以通过单例类型属性进行类型区分)
(单例类型即只有一个实例的类型,可看作是仅有一个值的字符串/数字字面量类型)
索引类型
代码示例 src/demos/tk.tsx
使用索引类型,TypeScript可以检查使用了动态属性名的代码。通常用于对象的不常规处理。
索引类型查询操作符keyof可以获取对象上所有属性名,索引访问操作符K[T]可以获取对象上特定属性的类型。
在示例代码中,由于K extends keyof T,因此K的取值是有限的,等价于K : “id” | “age” | “school” | “location”,并且K的取值随着T的key变化而变化。
映射类型
代码示例 src/demos/tk.tsx
映射类型是TypeScript提供的一种以相同方式从旧的类型中转换出新的类型的方式,例如将所有属性转换为只读属性或可选属性。
TypeScript集成了一些已实现的映射类型。
当然开发者也可以根据业务需求利用索引类型等方式实现自定义的映射类型。
3.模块
代码示例 src/module/*
从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。
导出
任何声明(比如变量,函数,类,类型别名或接口)、语句都能够通过添加export关键字来导出。
导出时可以通过as关键字来重命名。
当一个模块包含多个模块时,可以通过export *来导出所有模块。
默认导出
每个模块都可以有至多一个默认导出,用export default来标记。
导入
使用import关键字导入其他模块中导出的内容,使用as关键字重命名,使用*导入路径下所有导出内容。
当导入默认导出的模块时,导入时可以自由命名。
直接import “xxx”也是一种导入方式,但是有副作用的导入。导入文件会对整个项目生效,需要谨慎使用。通常仅在项目入口文件里导入全局css时会用到。
相对导入和非相对导入
根据模块引用是相对的还是非相对的,TypeScript会以不同的方式解析导入模块。
以/,./或../开头的被认为是相对导入。 例如:
- import Entry from “./components/Entry”;
- import { DefaultHeaders } from “../constants/http”;
- import “/mod”;
所有其它形式的导入都被当作非相对导入。 例如:
- import { Component } from “@angular/core”;
export = 和 import module = require()
为了支持CommonJS和AMD中的exports,TypeScript提供了export = 的导入声明。
当使用export =导出模块时,必须要使用import moduleName = require(“xxxx”)来导入。
现有React + Antd前端项目中常用的一些TS
1.Form表单
代码示例 src/template/form.tsx
2.Table表格
代码示例 src/template/table.tsx