当前位置: 首页 > 科技观察

TypeScript备忘单:如何在React中完美使用它?

时间:2023-03-12 12:56:09 科技观察

前言一直以来,很多ssh身边的朋友都对React中TS是怎么用的感到困惑。他们开始慢慢厌恶TS,觉得各种莫名其妙的问题降低了开发效率。其实,如果你熟练使用,TS只是在第一次开发时多花一点时间写类型,在后续的维护和重构中就会发挥它的神奇作用。还是强烈建议将其用于长期维护项目。其实我一直都知道英文版有一个很好的备忘录。我想直接把它推荐给我的朋友。不过很多人看英文都头疼,中文翻译版点进来,就是这个场景:既然这样,自己动手吧。结合英文原版中的一些例子,进行了一些扩充,总结了这篇备忘录。阅读本文的前提是:熟悉React的使用。熟悉TypeScript中的类型。本文将重点以ReactHook为例,当然大部分类型知识都是通用的。也就是说,本文着重于“React与TypeScript的结合”,而不是基础知识,这些基础知识可以通过阅读文档来学习。工具TypeScriptPlaygroundwithReact:可以在线调试React+TypeScript,只能调试类型,不能运行代码Stackblitz:云开发工具,可以直接运行React代码和预览CreateReactAppTypeScript:本地使用脚手架生成React+TS项目选择你可以使用你更喜欢的调试工具。ComponentProps先看几种定义常用Props的类型:基本类型typeBasicProps={message:string;count:number;disabled:boolean;/**数组类型*/names:string[];/**使用"joinType"仅限于以下两种"stringliteral"类型*/status:"waiting"|"success";};objecttypetypeObjectOrArrayProps={/**如果不需要使用特定的属性,可以像这样做一个模糊的规范Object?不推荐*/obj:object;obj2:{};//同上/**具有特定属性的对象类型?推荐*/obj3:{id:string;title:string;};/**对象数组😁常用*/objArr:{id:string;title:string;}[];/**key可以是任意字符串,取值仅限于MyTypeHere类型*/dict1:{[key:string]:MyTypeHere;};dict2:Record;//和dict1基本一样,使用TS自带的Record类型。}函数类型typeFunctionProps={/**任意函数类型?不建议不能指定参数和返回值类型*/onSomething:Function;/**不带参数的函数不需要返回值😁常用*/onClick:()=>void;/**带有函数😁的参数很常用*/onChange:(id:number)=>void;/**另一个函数语法参数是React的按钮事件😁很常用*/onClick(event:React.MouseEvent):void;/**可选参数类型😁很常用*/optional?:OptionalType;}React相关类型exportdeclareinterfaceAppProps{children1:JSX.Element;//不考虑数组children2:JSX.Element|JSX不推荐.Element[];//不推荐不考虑stringchildrenchildren4:React.ReactChild[];//稍微好一点但不考虑nullchildren:React.ReactNode;//?包含所有子函数Children:(name:string)=>React.ReactNode;//?函数返回React节点样式?:React.CSSProperties;//?推荐内联样式时使用//?推荐native所有propstypesthatcomewiththebuttontag//也可以在泛型位置传入组件提取组件的props类型propsprops:React.ComponentProps<"button">;//?推荐使用上一步进一步提取Native的onClick函数类型//此时函数的第一个参数会自动推断为React的点击事件类型onClickButton:React.ComponentProps<"button">["onClick"]}function最简单的组件:interfaceAppProps={message:string};constApp=({message}:AppProps)=>

{message}
;包含children:如果你使用React.FC的内置类型,它不仅会包含你定义的AppProps还会自动添加一个children类型,以及会出现在其他组件上的types://等同于AppProps&{children:React.ReactNodepropTypes?:WeakValidationMap

;contextTypes?:ValidationMap;defaultProps?:Partial

;displayName?:string;}//使用interfaceAppProps={message:string};constApp:React.FC=({message,children})=>{return(<>{children}

{消息}
)};Hooks@types/react包在16.8以上的版本开始支持HooksuseState如果你的默认值已经可以描述类型,那么你不需要手动声明类型,交给TS可以自动推断://val:booleanconst[val,toggle]=React.useState(false);toggle(false)toggle(true)如果初始值为null或undefined,必须通过genericsexpectedtype手动传给你。const[user,setUser]=React.useState(null);//稍后...setUser(newUser);这样也可以保证当你直接访问用户上的属性时,会提示可能为null。可以通过使用可选链接语法(TS3.7及更高版本支持)来避免此错误。//?okconstname=user?.nameuseReducer需要使用DiscriminatedUnion来标记Action的类型。constinitialState={count:0};typeACTIONTYPE=|{type:"increment";payload:number}|{type:"decrement";payload:string};functionreducer(state:typeofinitialState,action:ACTIONTYPE){switch(action.type){case"increment":return{count:state.count+action.payload};case"decrement":return{count:state.count-Number(action.payload)};default:thrownewError();}}functionCounter(){const[state,dispatch]=React.useReducer(reducer,initialState);return(<>Count:{state.count}dispatch({type:"decrement",payload:"5"})}>-dispatch({type:"increment",payload:5})}>+/>);}"歧视联盟“一般都是联合类型,每一种类型都需要通过特定字段来区分。当您传入特定类型时,将自动匹配和推断剩余的类型负载。这样:当你写的类型匹配到减量时,TS会自动推断对应的payload应该是string类型。当你写的类型匹配增量时,payload应该是number类型。这样你dispatch的时候,输入对应的类型,它会自动提示你输入剩余的参数类型。这里useEffect主要需要注意的是useEffect传入的函数,它的返回值要么是method(清理函数)要么是undefined,其他情况都会报错。一种比较常见的情况是我们的useEffect需要执行一个async函数,比如://?//Type'Promise'providesnomatch//forthesignature'():void|undefined'useEffect(async()=>{constuser=awaitgetUser()setUser(user)},[])虽然async函数中没有明确的返回值,但是async函数默认会返回一个Promise,这会导致TS报错。建议改写成这样:useEffect(()=>{constgetUser=async()=>{constuser=awaitgetUser()setUser(user)}getUser()},[])还是使用自执行函数?不推荐,可读性不好不错。useEffect(()=>{(async()=>{constuser=awaitgetUser()setUser(user)})()},[])useRef这个Hook在很多情况下是没有初值的,所以可以在返回对象当前属性的类型:constref2=useRef(null);以一个按钮场景为例:(<>Focustheinput/>);}当onButtonClick事件被触发时,可以肯定inputEl也是有价值的,因为组件是同层渲染的,但是还是需要冗余的非空判断。有办法解决这个问题。constref1=useRef(空!);空!这个语法是一个非空断言,后面跟着一个值就代表你确定它有值,所以当你使用inputEl.current.focus()时,TS不会报错。但是,这种语法很危险,需要尽量减少。在大多数情况下,inputEl.current?.focus()是更安全的选择,除非该值真的不可能为空。(比如在使用前赋值)useImperativeHandle建议使用自定义的innerRef代替原来的ref,否则使用forwardRef的类型会很复杂。typeListProps={innerRef?:React.Ref<{scrollToTop():void}>}functionList(props:ListProps){useImperativeHandle(props.innerRef,()=>({scrollToTop(){}}))returnnull}结合只是useRef的知识是这样使用的:()}很完美,不是吗?CustomHook如果想以useState的形式返回一个数组给用户,一定要记得在合适的时候使用asconst,将返回值标记为一个Constants,告诉TS数组中的值不会被删除,改变顺序等等...不然你的每一项都会被推断为“所有可能类型的联合类型”,影响用户的使用。exportfunctionuseLoading(){const[isLoading,setState]=React.useState(false);constload=(aPromise:Promise)=>{setState(true);returnPromise.finally(()=>setState(false));};//?添加asconst会推断出[boolean,typeofload]//?,否则会是(boolean|typeofload)[]return[isLoading,load]asconst;[]}顺便说一句,如果你使用ReactHook来写一个库,别忘了导出类型供用户使用。ReactAPIforwardRef函数式组件默认是不能加ref的,它不像类组件那样有自己的实例。该API通常由功能组件用于接收来自父组件的引用。所以需要标记实例类型,即父组件通过ref可以获得什么类型的值。typeProps={};exporttypeRef=HTMLButtonElement;exportconstFancyButton=React.forwardRef((props,ref)=>({props.children}));由于本例中ref直接转发给按钮,因此直接将类型标记为HTMLButtonElement即可。父组件可以通过这样调用它来获取正确的类型:exportconstApp=()=>{constref=useRef()return()}