TypeScript in React
为什么使用TypeScript?
日常开发中弱类型JavaScript的痛点
- 引用的组件/函数不知道可接收的参数以及参数类型-----各种找文档----甚至深入到源码
- 复杂数据的数据流转难以追踪----各种Debugger或者日志排查
- BFF/后端接口字段以及字段类型不明确----各种找文档----负责人
- 底层依赖的接口类型改动----前端全局搜索替换改动的地方----调试
TypeScript为了类型定义而诞生,具有以下优势
- 定义组件的属性以及函数的参数,代码即文档展示对应的类型
- 对复杂数据定义类型,在数据流转时也能清晰的知道数据类型,便于追踪
- 对后端接口,规范定义类型,更易于维护
- 静态类型检查,在coding阶段发现问题
- 强大的IDE自动补全/检查
带来的收益和成本
- 收益相关:问题提前暴露、复杂数据流转追踪、IDE的智能提示、强大的Type系统
- 成本相关: 增加学习和类型维护成本
TypeScript in React
开发环境: ESLint+Prettier+TypeScript Playground with React
ESLint
- ESLint --Javascript Lint,规范代码质量,提供开发效率
- 安装依赖
- Eslint: Javascript 代码检测工具
- @typescript-eslint/eslint-plugin:TS规则列表,可以打开或关闭每一条规则
- @typescript-eslint/parser:将TS转化为ESTree,这样才能被eslint检测到
- 配置 .eslintrc
- Parser: 指定ESLint使用的语法分析器:如Esprima、Babel-ESLint、@typescript-eslint/parser 默认Esprima
- parserOptions: { ecmaVersion: 6 // es版本
sourceType: ‘module’, // 设置为 “script” (默认) 或 “module”(ES6)。ecmaFeatures: { // 这是个对象,表示你想使用的额外的语言特性: jsx: true // 启用 JSX } },
- extends:继承的规则,可以在rules进行覆盖
- Plugins: 使用第三方插件
- rules:规则("off"或0 -关闭规则;“warn” 或1 - 开启规则, 使用警告 程序不会退出;"error"或2 - 开启规则, 使用错误 程序退出)
Prettier
- 统一团队的编码风格,保证代码的可读性,可设置保存自动格式化
- 安装依赖
- prettier:按照配置格式化代码
- eslint-config-prettier:禁用任何可能干扰现有 prettier 规则的 linting 规则
- Eslint-plugin-prettier: 作为ESLint的一部分运行Prettier分析
- 配置 .eslintrc.js
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"semi": true,
"tabWidth": 4,
"useTabs": false
}
工具:TypeScript Playground with React
可以在线调试React+TypeScript ,注意:只能调试类型,不能运行代码
VSCode 编辑器
在 workspace settings 中配置检测文件范围,确保 React 项目中 .ts 和 .tsx 文件有自动修复功能。
{
"eslint.validate": ["typescript", "typescriptreact"]
}
配置 tsconfig.json
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
React 组件声明
- 类组件,使用React.Component<P,S> 和React.PureComponent<P,S,SS>进行定义
- P是Props类型,S为State类型,SS为 Snapshot 返回的值
- 类组件除了需要约束Props 参数外,还需要给State 进行定义。如果有可选参数,需要定义默认值,类组件中使用 static 关键字。
- React 是单向数据流,Props 是不允许在子组件内被修改的,那我们需要手动为每个属性都加上Readyonly?
- ts playground中试试
- React 函数组件,使用React.FunctionComponent定义函数组件,一种是直接给Props写定义
- React.FC在定义内部已经定义Children 的类型和函数返回值,可以直接用Children
- Props则需要自己定义Children类型
- ts playground中试试
- 无状态组件:在 React 的声明文件中 已经定义了一个 SFC 类型
// 无状态组件
interface IProps {
onClick(event: MouseEvent<HTMLDivElement>)=> void
}
const Button: React.SFC<IProps> = ({ onClick, children }) => {
return <div onClick={onClick}>{children}</div>
}
- JSX.Element vs ReactNode vs ReactElement
ReactElement 是具有类型和属性的对象,通过执行 React.createElement 或是转译 JSX 获得
ReactNode是多种类型的集合,是ReactElement,ReactFragment,字符串,ReactNodes的数字或数组,或者为Null,未定义或 Bool 值
类组件类型定义:通过 render() 返回 ReactNode,比 React 的实际值范围更宽松
函数组件类型定义:返回 JSX.Element,也比 React 的实际值范围更宽松
//React.ReactElement
const ComA: React.ReactElement<myComponent> = < MyComponent />
// 接受一个可以在 Props 起作用,并使用 JSX 渲染的组件
const ComB: React.Component< ComProps > = ComA;
// Render ComB with some props:
<ComB {... ComProps} />;
// React.ReactNode:渲染一些像 JSX 或者是 string 的内容
interface Props = {
header: React.ReactNode;
body: React.ReactNode;
};
const MyComPonent2:React.FunctionComponent<Props> = (props)=>{
const { header, body, footer } = props
return (
<>
{header}
{body}
</>
)
}
<MyComponent2 header={<h1>Header</h1>} body={<i>body</i>} />
- 实现一个通用组件 ts playground中试试
React Hook
- useState ts playground中试试
- useEffect
//2. useEffect: useEffect 传入的函数,它的返回值要么是一个方法(清理函数),要么就是undefined,其他情况都会报错。
//asyanc 默认返回一个promise ,导致ts报错
useEffect(async () => {
const user = await getUser()
setUser(user)
}, [])
//推荐用法
useEffect(()=>{
const getUser = async ()=>{
const user = await getUser()
setUser(user)
}
getUser()
},[])
- useRef
- 使用 useRef 时,我们一般有两种方式去创建没有初始值的 Ref 容器。
// option 1
const ref1 = useRef<HTMLInputElement>(null);
// option 2
const ref2 = useRef<HTMLInputElement>(null!);
// option 3
const ref3 = useRef<HTMLInputElement | null>(null);
▪ 两者的区别?RefObject VS MutableRefObject
▪ ts playground中试试
- forwardRef
- 因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
- ts playground中试试
- useImperativeHandle
- 自定义暴露给父组件的值
- ts playground中试试
- useReducer
- ts playground中试试
- 自定义Hook
- 使用自定义钩子,可以提取和复用组件逻辑
- 写自定义 Hook 时,Hook 返回一个数组,则要避免类型推断
- 需要自定义返回值 or 将返回的数组断言 const
- ts playground中试试
事件处理
- Event事件对象类型
- 所有的类型定义都有相同的格式:React.事件名
- ts playground中试试
//不在乎事件类型,可以使用React.SyntheticEvent (@types/react/index.d.ts)所有的事件都是他的子类型
//1. 表单事件
const onSumbit = (e:React.ChangeEvent<HTMLFormElement>)=>{...}
//2. input 事件
const onChange = (e:React.ChangeEvent<HTMLInputElement>)=>{...}
//其他事件
// 1. ClipboardEvent<T = Element> 剪贴板事件对象
// 2. DragEvent<T = Element> 拖拽事件对象
// 3. ChangeEvent<T = Element> Change 事件对象
// 4. KeyboardEvent<T = Element> 键盘事件对象
// 5. MouseEvent<T = Element> 鼠标事件对象
// 6. TouchEvent<T = Element> 触摸事件对象
// 7. WheelEvent<T = Element> 滚轮事件对象
// 8. AnimationEvent<T = Element> 动画事件对象
// 9. TransitionEvent<T = Element> 过渡事件对象
- 事件处理函数类型
当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?-------EventHandler
EventHandler 接收 E ,其代表事件处理函数中 Event 对象的类型。
interface IProps {
onClick : MouseEventHandler<HTMLDivElement>,
onChange: ChangeEventHandler<HTMLDivElement>
}
Promise类型
在代码中,我们会遇到Async函数,调用的时候返回的是一个Promise对象,怎么定义呢?
Promise是一个泛型类型,T泛型变量用于确定使用 then 方法时接收的第一个回调函数(onfulfilled)的参数类型
interface IResponse<T> {
message: string,
result: T,
success: boolean,
}
async function getResponse (): Promise<IResponse<number[]>> {
return {
message: '获取成功',
result: [1, 2, 3],
success: true,
}
}
getResponse()
.then(response => {
console.log(response.result)
})
实用技巧
- Interface or type
- 扩展:interface基于extends,type 基于交叉类型 &
- type 可以声明基本类型别名,联合类型,元组等类型,配合 typeof 获取实例类型。
- interface 能够声明合并
- 能用interface实现优先interface
- 类型提取(index type、mapped type、keyof)
interface UserInfo{
id:number
name:string
status: 1|2|3|4
}
// index type
type UserStatus = {
id: UserInfo['id']
status:UserInfo['status']
}
// mapped type
type UserStatus = {
[K in 'id'|'status']:Userinfo[K]
}
// keyof
function getStatus<T extends { [key:string]:any },K extends keyof T>(obj:T,key:K):T[K]{
return obj[key]
}
const status = getStatus(UserInfo,'status')
- 巧用 typeof 快速定义接口类型
const INIT_OPTIONS = {
id:101,
name:'banggan',
age:26,
tel:1809999999,
};
interface Options {
id: number
name: string
age:number
tel:number
}
type Options = typeof INIT_OPTIONS
// Ts 中的 typeof 可以用来获取一个真实的变量、对象的类型,也可以用来获取函数的类型
- 工具泛型 ts playground中试试
const info = {
name:'banggan',
age:26,
sex:'man'
location:'beijing'
tel:88888888
}
// sex 需要数字映射----枚举
enum SEX_MAP{
'man',
'woman'
}
const info1 = {
sex:SEX_MAP.man
}
// 字段重构为数字映射,之前的字符串则表明处理----keyof valueof
// keyof返回一个类型里面所有的key组成的联合类型
// ts没有valueof关键字,T[keyof T]就是对标keyof的valueof的效果
interface SEX_MAP1 {
man:0,
woman:1
}
interface Info {
name: string
age: number
sex: keyof SEX_MAP1 | SEX_MAP1[keyof SEX_MAP1]
}
const info2: Info = {
name:'sb',
age:1,
sex: 'man'
}
const oldInfo: Info={
name:'1',
age: 1,
sex: 0
}
// info全部为string Record<T,U> T 传入的jey U表示对于key的value的类型
const s: Record<keyof Info,string> = {
name:'123',
age:'111',
sex:'man'
}
type MyRcord<T extends keyof any,U> = {
[k in T]:U
}
// 全部变为可选
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
// 全部为必填
type MyRequired<T> ={
[P in keyof T]-?:T[P]
}
// 全部变为可读
type MyReadonly<T> = {
readonly [P in keyof T] :T[P]
}
// 如果只想取部分属性 进行required等操作----Pick
// Pick是指从T里面挑几个key,如Pick<type1, ‘key1’ | ‘key2’>。先把你所希望变成可选的key选出来,再交叉类型补上剩下的
type Info1 = Partial<Pick<Info,'name' | 'sex'>> & {age:number}
const info3: Info1 = {
name:'1111',
age:1
}
//删除某个属性
// Omit和pick相反,选出一个类型里面除了这些给定key的剩下的key。如Omit<type1, ‘key1’ | ‘key2’>,表示选取type1中除了key1和key2的其他key。
type Info2 = Partial<Pick<Info,'name'|'sex'>> & Omit<Info,'name'|'sex'>
//pick 的实现----in
type MyPick<T, K extends keyof T> = {
[P in K]:T[P]
}
//限制的是T里面存在的key--- 约束范围放大---condition type
type SuperPick<T,K extends keyof any> = {
[P in K extends keyof T ? K:never]:T[P]
}
// condition type表示条件类型,类似三元表达式,前面的条件部分语句需要使用extends,条件就是 A extends B
type isNumber<T> = T extends number ?T:never
type test1 = [isNumber<1>,isNumber<number>,isNumber<'1'>]
// omit的实现 pick出来K里面的key集合,再pick剩下的key
type MyExclude<T,U> = T extends U ? never:T
type MyOmit<T,K extends keyof any> = Pick<T,MyExclude<keyof T,K>>
// infer ---- 表示在condition type 的条件语句中待推断的类型变量,例如returntype就是靠infer实现的
type MyReturnType<T> = T extends (...args:any[])=> infer P ? P:any
// 解promise 取数组类型的item都可以这样操作
//一个对象两种key组合形式---联合类型解决
// 像document.queryselector这种,确实不知道返回值
// is类型断言
function isDiv(ele:Element |null):ele is HTMLDivElement{
return ele && ele.nodeName === "DIV"
}
function isCanvas(ele:Element |null):ele is HTMLCanvasElement{
return ele && ele.nodeName === "CANVAS"
}
function commonQuery(selector:string){
const ele = document.querySelector(selector)
if(isDiv(ele)){
console.log(ele.innerHTML)
}else if (isCanvas(ele)){
console.log(ele.getContext)
}
}
// window window下的属性
interface Window {
a:number
}