TypeScript in React

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,规范代码质量,提供开发效率

  • 安装依赖

    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

  • 统一团队的编码风格,保证代码的可读性,可设置保存自动格式化

  • 安装依赖

    • 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
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值