Typescript在React中的最佳实践

    43

背景

UI=fn(state)

我们的组件函数接收一个状态stage作为参数,函数的调用结果就是当前我们视图的UI,所以react最重要的就是fn和state这两部分、所以ts在react中最佳实践差不多等于在函数最佳实践,区别就是多了个jsx语法,TS开发确实比js开发多花时间去编写类型,但是后续维护,重构和代码提示方面确实收益大于话费的时间的。

目录

  1. typescript环境配置
  2. ts与react的基础知识
  3. 函数式组件的定义
  4. Hooks的定义
  5. HTML元素的定义

环境配置

目前的javascript项目基本都是webpack构建的,对于一个javascript项目我们迁移到typescript我们只需要以下几个重要步骤。

  1. 安装依赖以及类型定义文件
npm install typescript -D

在开发中我们难免用到第三方的npm模块,这些模块不一定是ts开发的,也不一定提供类型定义文件

我们可以安装对应的类型定义文件

@types/包https://www.typescriptlang.org/dt/search?search=

npm install @types/react -D

npm install @types/react-dom -D
  1. 配置typescript

tsc --init 生成tsconfig.json

https://www.typescriptlang.org/tsconfig

仅仅给idea提示用的

声明文件

装新包用ts写的一般都会提供声明文件,否则我们需要自己再

Package.json typings

有的项目没有自带声明文件,比如

react本身

https://www.typescriptlang.org/dt/search?search=

用@types/

@types/react

@types/react-dom

{

  // ...

  "compilerOptions": {

    "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度

    "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置

    "diagnostics": true, // 打印诊断信息 

    "target": "ES5", // 目标语言的版本

    "module": "CommonJS", // 生成代码的模板标准

    "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",

    "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",

    "allowJS": true, // 允许编译器编译JS,JSX文件

    "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用

    "outDir": "./dist", // 指定输出目录

    "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构

    "declaration": true, // 生成声明文件,开启后会自动生成声明文件

    "declarationDir": "./file", // 指定生成声明文件存放目录

    "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件

    "sourceMap": true, // 生成目标文件的sourceMap文件

    "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中

    "declarationMap": true, // 为声明文件生成sourceMap

    "typeRoots": [], // 声明文件目录,默认时node_modules/@types

    "types": [], // 加载的声明文件包

    "removeComments":true, // 删除注释 

    "noEmit": true, // 不输出文件,即编译后不会生成任何js文件

    "noEmitOnError": true, // 发送错误时不输出任何文件

    "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用

    "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块

    "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现

    "strict": true, // 开启所有严格的类型检查

    "alwaysStrict": true, // 在代码中注入'use strict'

    "noImplicitAny": true, // 不允许隐式的any类型

    "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量

    "strictFunctionTypes": true, // 不允许函数参数双向协变

    "strictPropertyInitialization": true, // 类的实例属性必须初始化

    "strictBindCallApply": true, // 严格的bind/call/apply检查

    "noImplicitThis": true, // 不允许this有隐式的any类型

    "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)

    "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)

    "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)

    "noImplicitReturns": true, //每个分支都会有返回值

    "esModuleInterop": true, // 允许export=导出,由import from 导入

    "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块

    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入

    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录

    "paths": { // 路径映射,相对于baseUrl

      // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置

      "jquery": ["node_modules/jquery/dist/jquery.min.js"]

    },

    "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错

    "listEmittedFiles": true, // 打印输出文件

    "listFiles": true// 打印编译的文件(包括引用的声明文件)

  }

}
import * as React from 'react'

import * as ReactDOM from 'react-dom'

//allowSyntheticDefaultImports
//tsconfig.json

{

  "include": [

    "src/**/*"

  ],

  "compilerOptions": {

    "target": "es5",

    "module": "es6",

    "noEmit": true,//不生成类型

    "noImplicitAny": true, //不能有隐式any

    "allowTernary": true,

    "allowShortCircuit": true,

    "allowSyntheticDefaultImports":true,//允许默认导入

    "esModuleInterop": true,//esmodule支持

    "strict": true,

    "lib": [

      "dom",

      "es2015"//dom、html类型定义 es定义

    ],

    "jsx": "react-jsx"//最重要

  }

}

此时我们已经可以用tsc来编译我们的ts代码了,tsconfig知识给vscode

  1. 修改构建配置

这一步我们主要修改webpack和babel的相关配置

//webpack.config.js

const path = require('path');



module.exports = {

    resolve: {

        extensions: ['.tsx', '.ts', '.js']

    },

    module: {

        rules: [{

            test: /\.(ts|js)x?$/,

            exclude: /node_modules/,

            loader: 'babel-loader',

        }],

    }

};

对于typescript的编译我们主流方案有

编译方案 缺点 优点
awesome-typescript-loader 最近一次维护3年前
ts-loader es6语法无法转化、每次修改文件会重新去编译ts文件 类型校验加编译
babel/babel-loader 没有类型检查功能,因此语法正确但无法通过 TypeScript 类型检查的代码可能会成功转换 部分语法无法编译常量枚举(7.15.0已支持) tsc --noEmit --watch 现有最佳方案 @babel/preset-typescript 使用 babel,不仅能处理 typescript,之前 babel 就已经存在的 polyfill 功能也能一并享受。并且由于 babel 只是移除类型注解节点,所以速度相当快。 不经过tsc直接转化成js,自带缓存,速度快
swc-loader rust写的不会校验类型 速度快
esbuild/esbuild-loader go写的 不会校验类型 速度快

没有类型检查功能,因此语法正确但无法通过 TypeScript 类型检查的代码可能会成功转换

tsc --noEmit --watch

img

目前最佳方案是使用babel-loader转化语法,使用tsc去检查类型

  1. 其他准备
    1. 使用到jsx语法的文件后缀为.tsx,普通的js语法后缀为.ts
    2. 全局变量或者扩展window对象属性在typings/global.d.ts文件里
//png资源

declare module "*.png";

//扩展window对象

declare global {

  interface Window {

    MyVendorThing: MyVendorType;

  }

}

基础知识

  1. interface和type该用哪个

inerfacetype都可以定义类型,所以我们该用哪个,

表现上来说2者可以实现的功能都可以互相实现,唯一的声明合并方面,type不行即type 类型不能二次编辑,而 interface 可以随时扩展

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口

  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type的约束性更强

interface Person {

  age: number;

}



interface Person {

  name: string;

}





type Person {

  age: number;

}



type Person {

  name: string;

}

//TS2300: Duplicate identifier 'Person'.
  1. {}和Record<string,any>

表示没有成员的对象

我们表示一个对象可以Record<string,any>

  1. unknown和any

anyscript

使用any代表我们放弃了所有的类型检查,推荐使用Unknown

不缩小类型,我们无法对unknown 类型执行任何操作

unknown 和 any 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。

使用as

使用typeof

断言错了时语法能通过检测,但是运行的时候就会报错了!

  1. JSX.Element、ReactNode、ReactElement

JSX.Element 是React.createElement 或是转译 JSX 获得的对象的类型等价于ReactElement

JSX.Element React.createElement创建的jsx对象

React.ReactNode 所有可能返回值的集合,

他的声明如下,是一个联合类型

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

我们可以把jsx赋值给他,在react世界里相当于any

  1. 获取未导出的类型

这种方式的一个好处是获取到的是真实的类型声明, 导出的类型声明有时候是不完整的

import { Button } from 'antd';



type ButtonProps = React.ComponentProps<typeof Button>;
  1. 3个小知识
    1. ??代替 ||
    2. 魔法值
const enum Gender {

  female = '1',

  male = '2',

}



function getSex(sex: string) {

  if (sex === Gender.female) {

    console.log(sex);

  }

}

函数式组件的相关声明

现在重尚函数式编程,react也逐渐在向函数式编程思想迈进,当然这个函数式不是函数组件的意思,只是函数组件的编程思想运用到了函数式

一个标准的函数式组件是这样定义的

type PersonProps = {

  name?: string;

  age: number;

  onClick: () => void;

};



function Person(props: PersonProps): JSX.Element {

  const { name = '', age } = props;

  return (

    <div onClick={() => console.log('hello')}>

      {name}-{age}

    </div>

  );

}

props接口推荐以 ComponentName+props命名 ComponentNameProps

返回类型其实可以不写,ts可以自动推断我们的返回类型

function App() {

  const isShow = false;

  return isShow ? <input type='text' /> : null;

}

img

很多人喜欢用React.FC定义类型,这是不推荐的做法,

https://github.com/facebook/create-react-app/pull/8177

  • React.FC 显式地定义了返回类型,其他方式是隐式推导的;如果返回类型是数组类型检查就会不通过提供子项的隐式定义,即使您的组件不需要有子项。

  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;

  • React.FC 为 children 提供了隐式的类型(ReactNode)

比如以下就会报错

const App: React.FC = props => props.children



const App: React.FC = () => [1, 2, 3]



const App: React.FC = () => 'hello'

children推荐用React.ReactNode定义

interface ButtonProps {

  children: React.ReactNode;

}



function Button({ children }: ButtonProps) {

  return <button>{children}</button>;

}

react也为我们封装了一个类型工具

type ButtonProps = {

  onClick: () => void;

};



function Button(props: PropsWithChildren<ButtonProps>) {

  const { children } = props;

  return <button onClick={() => console.log('onClick')}>{children}</button>;

}

归根到底

type AppProps = { message: string }; /* 也可用 interface */

const App = ({ message }: AppProps) => <div>{message}</div>; // 无大括号的箭头函数,利用 TS 推断。

Hooks的相关声明

useState

如果初始值已经可以说明类型,那么不用手动声明类型,TS 会自动推断类型

如果没有初始值需要给null初始值,需要给定类型

但是在使用时大家通常会加user?.id

通常没有初始值,需要初始化空对象,我们可以类型断言来骗ts编译器

//简单类型的初始值

const [val, setVal] = useState(false);

//复杂类型的初始值

const [user, setUser] = useState({ name: '张三', age: 18, password: '123456' });

const handleInitial =(val:typeof user) => {

    setUser(val);

};

//一般情况下我们没有初始值,需要调用接口去请求数据

const [user, setUser] = useState<UserType | null>(null);//user?.id

const [user, setUser] = useState<UserType>({} as UserType);//better

useRef

通常两种用法

  1. 只读的HTML引用
function App() {

  const divRef = useRef<HTMLDivElement>(null);//必须给null初始值,类型不用给null包括在里面



  useLayoutEffect(() => {

    if (!divRef.current) throw Error('divRef is not assigned');

    divRef.current.innerHTML = 'hello world';

  });



  return <div ref={divRef}>hello</div>;

}

如果我我们的ref不是有条件的渲染,divRef永远不会为空

function App() {

  const divRef = useRef<HTMLDivElement>(null!);



  useLayoutEffect(() => {

    divRef.current.innerHTML = 'hello world';

  });



  return <div ref={divRef}>hello</div>;

}

一般的都是HTMLButtonElement类似的,如果实在不清楚我们可以写**HTMLElement,**编译器会提示我们正确的类型。

img

  1. 可变的存储变量

和useState差不多用法

function App() {

  const timer = useRef(0);



  useLayoutEffect(() => {

    timer.current = setTimeout(() => {

      console.log('timer');

    }, 1000);

    return () => clearTimeout(timer.current);

  });



  return <div>hello</div>;

}

useContext

推断类型

const AppContext = createContext({

  authenticated: true,

  lang: 'en',

  theme: 'dark',

});

const MyComponent = () => {

  const appContext = useContext(AppContext); //inferred as an object

  return <h1>The current app language is {appContext.lang}</h1>;

};

给定类型

type Theme = 'light' | 'dark';

const ThemeContext = createContext<Theme>('dark');

const App = () => {

  const theme = useContext(ThemeContext);

  return <div>The theme is {theme}</div>;

};

img

useEffect、useLayoutEffect、useMemo、useCallback

useEffect、useLayoutEffect一个参数,返回值必须函数或者undefined(void)

const multiply = React.useCallback((value) => value * 2, []);//给类型

自定义hook

需要注意,自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 所有Union 类型,而我们实际需要的是数组里里每一项的具体类型,需要手动添加 const 断言 进行处理:告诉ts这是个常量不会修改顺序删除

function useToggle() {

  const [state, setState] = useState(false);

  const toggle = () => setState(!state);

  return [state, toggle];

}

img

function useToggle() {

  const [state, setState] = useState(false);

  const toggle = () => setState(!state);

  return [state, toggle] as const;

}

img

HTML

HTML属性

button有很多属性 type 我们要一个个加吗

可以用

  1. ButtonHTMLAttributes
  2. ComponentPropsWithoutRef
type inputProps = ButtonHTMLAttributes<HTMLButtonElement>;



type inputProps = ComponentPropsWithoutRef<'button'>

function Button(props: inputProps) {

  return <button {...props}>{props.children}</button>;

}

如果要扩展属性也很方便使用 interface 或者 type

interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {

  customProp: string;

}



type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {

  customProp: string;

};



function Button(props: ButtonProps) {

  console.log(props.customProp);

  return <button {...props}>{props.children}</button>;

}

这部分主要是处理点击事件以及input类似的声明,众所周知,使用箭头函数内联在html中每次渲染都会生成新的函数,所以对性能是有影响的,但是内联的函数能非常正确的推断我们的类型

function App() {

  return (

    <>

      <button onClick={(e) => console.log(e)}>hello</button>

      <input onChange={(e) => console.log(e)} />

    </>

  );

}

img

事件类型

1.

React.XXXEvent<HTMLXXXElement>

ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent
  • 剪切板事件对象:ClipboardEvent<T = Element>

  • 拖拽事件对象:DragEvent<T = Element>

  • 焦点事件对象:FocusEvent<T = Element>

  • 表单事件对象:FormEvent<T = Element>

  • Change事件对象:ChangeEvent<T = Element>

  • 键盘事件对象:KeyboardEvent<T = Element>

  • 鼠标事件对象:MouseEvent<T = Element, E = NativeMouseEvent>

  • 触摸事件对象:TouchEvent<T = Element>

  • 滚轮事件对象:WheelEvent<T = Element>

  • 动画事件对象:AnimationEvent<T = Element>

  • 过渡事件对象:TransitionEvent<T = Element>

function App() {

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {

    console.log(e.target.value);

  };

  return <input onChange={handleChange} />;

}

事件处理函数

React.ReactEventHandler<HTMLXXXElement>
function App() {

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {

    console.log(e.target.value);

  };

  return <input onChange={handleChange} />;

}

axios封装

Axios react-query

Promise 是一个泛型类型

import type { AxiosRequestConfig } from 'axios';

import axios from 'axios';



interface Res<T = unknown> {

  code: number;

  data: T;

  message: string;

}

async function http<T>(config: AxiosRequestConfig): Promise<Res<T>> {

  const instance = axios.create({

    baseURL: process.env.BASE_URL,

    timeout: 10000,

    headers: { 'Content-Type': 'application/json;charset=UTF-8' },

    validateStatus:  ()=> true,

  });

  instance.interceptors.request.use(

    (config) => {

      return config;

    },

    (error) => {

      return Promise.reject(error);

    },

  );

  instance.interceptors.response.use(

    (response) => {

      return response;

    },

    (error) => {

      return Promise.reject(error);

    },

  );

  const { data } = await instance.request<Res<T>>(config);

  return data;

}



export default http;
export const getArticle = (params: ParamsType) => {

  return http<Article>({

    method: 'get',

    url: '/article/page',

    params,

  });

};
评论区
共有评论 0
暂无评论