TypeScript in React

avatar
前端 @北京字节跳动科技有限公司

为什么使用TypeScript?

日常开发中弱类型JavaScript的痛点

  • 引用的组件/函数不知道可接收的参数以及参数类型-----各种找文档----甚至深入到源码
  • 复杂数据的数据流转难以追踪----各种Debugger或者日志排查
  • BFF/后端接口字段以及字段类型不明确----各种找文档----负责人
  • 底层依赖的接口类型改动----前端全局搜索替换改动的地方----调试

TypeScript为了类型定义而诞生,具有以下优势

  • 定义组件的属性以及函数的参数,代码即文档展示对应的类型
  • 对复杂数据定义类型,在数据流转时也能清晰的知道数据类型,便于追踪
  • 对后端接口,规范定义类型,更易于维护
  • 静态类型检查,在coding阶段发现问题
  • 强大的IDE自动补全/检查

带来的收益和成本

  • 收益相关:问题提前暴露、复杂数据流转追踪、IDE的智能提示、强大的Type系统
  • 成本相关: 增加学习和类型维护成本

TypeScript in React

开发环境: ESLint+Prettier+TypeScript Playground with React

ESLint
  • ESLint --Javascript Lint,规范代码质量,提供开发效率
  • 安装依赖
  1. Eslint: Javascript 代码检测工具
  2. @typescript-eslint/eslint-plugin:TS规则列表,可以打开或关闭每一条规则
  3. @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
  • 统一团队的编码风格,保证代码的可读性,可设置保存自动格式化
  • 安装依赖
  1. prettier:按照配置格式化代码
  2. eslint-config-prettier: 禁用任何可能干扰现有 prettier 规则的 linting 规则
  3. 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)=> void } const Button: React.SFC = ({ onClick, children }) => { return

    {children}
    }

  • 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>} />

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(null); // option 2
    const ref2 = useRef(null!);
    // option 3 const ref3 = useRef<HTMLInputElement | null>(null);

  • 两者的区别?RefObject VS MutableRefObject

  • ts playground中试试

  • forwardRef

    • 因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
    • ts playground中试试
  • useImperativeHandle

  • useReducer

  • 自定义Hook

    • 使用自定义钩子,可以提取和复用组件逻辑
    • 写自定义 Hook 时,Hook 返回一个数组,则要避免类型推断
    • 需要自定义返回值 or 将返回的数组断言 const
    • ts playground中试试

事件处理

  • Event事件对象类型

    //不在乎事件类型,可以使用React.SyntheticEvent (@types/react/index.d.ts)所有的事件都是他的子类型 //1. 表单事件 const onSumbit = (e:React.ChangeEvent)=>{...} //2. input 事件 const onChange = (e:React.ChangeEvent)=>{...} //其他事件 // 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 = { [P in keyof T]?: T[P] } // 全部为必填 type MyRequired ={ [P in keyof T]-?:T[P] } // 全部变为可读 type MyReadonly = { 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 extends number ?T:never type test1 = [isNumber<1>,isNumber,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 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 }

作者:肖芳