译者注:react和ts结合使用的系统总结网上相关资料不多,于是就硬着头皮啃了英文版。译者英语烂的要死,勉强翻得。有不对的地方请多多指正。翻译自:https://react-typescript-cheatsheet.netlify.app/docs/basic/setup/ 英语基础好的推荐优先去看英文文档 翻译时间截至到2021年4月20日
一、基础 (一)开始之前 1、要求
2、Import React 1 2 import * as React from "react" ;import * as ReactDOM from "react-dom" ;
这里最推荐的导入react和react-dom的方式,如果在你的tsconfig.json中allowSyntheticDefaultImports为true,那么也可以使用import React from 'react'来导入。值得一提的是,在create-react-app脚手架的tsconfig.json中,默认将allowSyntheticDefaultImports设置为true
(二) 开始 1、组件属性定义(Props) (1)Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 type AppProps = { message: string; count: number; disabled: boolean; names: string[]; status: "waiting" | "success" ; obj: object; obj2: {}; obj3: { id: string; title: string; }; objArr: { id: string; title: string; }[]; onClick: () => void ; onChange: (id: number ) => void ; onClick(event: React.MouseEvent<HTMLButtonElement>): void ; optional?: OptionalType; children1: JSX.Element; children2: JSX.Element | JSX.Element[]; children: React.ReactNode; functionChildren: (name: string ) => React.ReactNode; onChange?: React.FormEventHandler<HTMLInputElement>; props: Props & React.ComponentPropsWithoutRef<"button" >; props: Props & React.ComponentPropsWithRef<MyButtonWithForwardRef>; };
(2)注意React.ReactNode的一个问题 1 2 3 4 5 6 7 8 9 10 11 type Props = { children: React.ReactNode; }; function Comp ({ children }: Props ) { return <div > {children}</div > ; } function App ( ) { return <Comp > {{}}</Comp > ; }
(3)选择JSX.Element还是React.ReactNode还是React.ReactElement? 详见https://github.com/typescript-cheatsheets/react/issues/129。
ReactElement是具有类型和属性的对象。
1 2 3 4 5 interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null; }
ReactNode是ReactElement,ReactFragment,字符串,ReactNodes的数字或数组,或者为null,未定义或布尔值:
1 2 3 4 5 6 7 type ReactText = string | number; type ReactChild = ReactElement | ReactText; interface ReactNodeArray extends Array <ReactNode> {} type ReactFragment = {} | ReactNodeArray; type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined ;
JSX.Element继承自ReactElement
1 2 3 4 5 declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { } } }
例如:
1 2 3 4 5 <p> <Custom> {true && "test" } </Custom> </p>
为什么类组件的render方法返回ReactNode,而函数组件返回ReactElement?
确实,他们确实返回了不同的东西。Component返回:
函数是“无状态组件”:
1 2 3 4 interface StatelessComponent<P = {}> { (props: P & { children?: ReactNode }, context?: any): ReactElement | null ; }
(4)interface(接口)和type(类型别名)该如何选择
2、函数组件(Function Component) (1)Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 type AppProps = { message: string; }; const App = ({ message }: AppProps ) => <div > {message}</div > ;const App = ({ message }: AppProps): JSX.Element => <div > {message}</div > ;const App = ({ message }: { message: string } ) => <div > {message}</div > ;
以前函数组件还有React.FC写法,现在仍然可以使用(但是不推荐)
3、类组件(Class Component) (1)Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type MyProps = { message: string; }; type MyState = { count: number; }; class App extends React .Component <MyProps , MyState > { state: MyState = { count: 0 , }; render ( ) { return ( <div> {this .props.message} {this .state.count} </div> ); } }
(2)Props和State的接口或者类型别名不再需要readonly标识其只读 1 2 3 4 5 6 type MyProps = { readonly message: string; }; type MyState = { readonly count: number; };
上面这种写法已经过时了,源代码中已经设置了State和Props只读,不需要readonly设置
(3)类组件中方法定义 与js写法基本一致,只是方法参数必须标注类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class App extends React .Component < { message: string }, { count : number }> { state = { count : 0 }; render ( ) { return ( <div onClick={() => this .increment(1 )}> {this .props.message} {this .state.count} </div> ); } increment = (amt: number ) => { this .setState((state ) => ({ count: state.count + amt, })); }; }
(4)类组件中属性定义 如果你需要在类中定义一个属性,像state一样声明即可,不必初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class App extends React .Component < { message: string; }> { pointer: number; componentDidMount ( ) { this .pointer = 3 ; } render ( ) { return ( <div> {this .props.message} and {this .pointer} </div> ); } }
4、Hooks (1)useState
1 2 const [val, toggle] = React.useState(false );
1 2 3 4 const [user, setUser] = React.useState<IUser | null >(null );setUser(newUser);
也可以这样初始化
1 2 3 const [user, setUser] = React.useState<IUser>({} as IUser);setUser(newUser);
(2)useReducer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const initialState = { count : 0 };type ACTIONTYPE = | { type : "increment" ; payload: number } | { type : "decrement" ; payload: string }; function reducer (state: typeof initialState, action: ACTIONTYPE ) { switch (action.type) { case "increment" : return { count : state.count + action.payload }; case "decrement" : return { count : state.count - Number (action.payload) }; default : throw new Error (); } } function Counter ( ) { const [state, dispatch] = React.useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type : "decrement" , payload : "5" })}> - </button> <button onClick={() => dispatch({ type : "increment" , payload : 5 })}> + </button> </> ); }
(3)useEffect 与js并无差别
这里主要需要注意的是,useEffect 传入的函数,它的返回值要么是一个方法 (清理函数),要么就是undefined ,其他情况都会报错。
比较常见的一个情况是,我们的 useEffect 需要执行一个 async 函数,比如:
1 2 3 4 5 6 7 useEffect(async () => { const user = await getUser() setUser(user) }, [])
虽然没有在 async 函数里显式的返回值,但是 async 函数默认会返回一个 Promise,这会导致 TS 的报错。
推荐这样改写:
1 2 3 4 5 6 7 useEffect(() => { const getUser = async () => { const user = await getUser() setUser(user) } getUser() }, [])
(4)useRef 有三种方式初始化useRef
1 2 3 4 5 6 const ref1 = useRef<HTMLElement>(null !);const ref2 = useRef<HTMLElement>(null );const ref3 = useRef<HTMLElement | null >(null );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React from "react" function TextInputWithFocusButton ( ) { const inputEl = React.useRef<HTMLInputElement|null >(null ); const onButtonClick = () => { inputEl.current && inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
(5)useImperativeHandle 略,不常用
(6)自定义hook中注意事项 如果想在自定义hook中返回数组,ts会把数组自动类型推论为联合类型。而不是数组内每一个元素拥有自己的类型。因此需要使用as const作处理。
React团队在返回多项数据时使用object类型而不是tuples(元组)类型。
1 2 3 4 5 6 7 8 9 export function useLoading ( ) { const [isLoading, setState] = React.useState(false ); const load = (aPromise: Promise <any> ) => { setState(true ); return aPromise.finally(() => setState(false )); }; return [isLoading, load] as const ; }
5、defaultProps设置 (1)ts+react你可能不需要defaultProps 根据这条推特https://twitter.com/dan_abramov/status/1133878326358171650,defaultProps将要在未来被抛弃,
解决办法就是使用在入口处使用对象默认值
1 2 3 4 type GreetProps = { age?: number }; const Greet = ({ age = 21 }: GreetProps ) =>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type GreetProps = { age?: number; }; class Greet extends React .Component <GreetProps > { render ( ) { const { age = 21 } = this .props; } } let el = <Greet age ={3} /> ;
(2)如果你非要用defaultProps 1 2 3 4 5 6 7 8 9 10 type GreetProps = { age : number } & typeof defaultProps; const defaultProps = { age: 21 , }; const Greet = (props: GreetProps ) => { }; Greet.defaultProps = defaultProps;
1 2 3 4 5 6 7 8 9 10 11 12 13 type GreetProps = typeof Greet.defaultProps & { age: number; }; class Greet extends React .Component <GreetProps > { static defaultProps = { age: 21 , }; } let el = <Greet age ={3} /> ;
(1)默认会自动进行类型推论 1 2 3 4 5 6 7 const el = ( <button onClick={(event ) => { }} /> );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type State = { text: string; }; class App extends React .Component <Props , State > { state = { text: "" , }; onChange = (e: React.FormEvent<HTMLInputElement>): void => { this .setState({ text : e.currentTarget.value }); }; onChange: React.ChangeEventHandler<HTMLInputElement> = (e ) => { this .setState({text : e.currentTarget.value}) } render ( ) { return ( <div> <input type="text" value={this .state.text} onChange={this .onChange} /> </div> ); } }
(2)表单onSubmit事件 如果不关系合成事件e的具体类型,可以使用通用类型React.SyntheticEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <form ref={formRef} onSubmit={(e: React.SyntheticEvent ) => { e.preventDefault(); const target = e.target as typeof e.target & { email: { value : string }; password: { value : string }; }; const email = target.email.value; const password = target.password.value; }} > <div> <label> Email: <input type="email" name="email" /> </label> </div> <div> <label> Password: <input type="password" name="password" /> </label> </div> <div> <input type="submit" value="Log in" /> </div> </form>
7、上下文(Context) (1)Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import * as React from "react" ;interface AppContextInterface { name: string; author: string; url: string; } const AppCtx = React.createContext<AppContextInterface | null >(null );const sampleAppContext: AppContextInterface = { name: "Using React Context in a Typescript App" , author: "thehappybug" , url: "http://www.example.com" , }; export const App = () => ( <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider> ); export const PostInfo = () => { const appContext = React.useContext(AppCtx); return ( <div> Name: {appContext?.name}, Author : {appContext?.author}, Url :{" " } {appContext?.url} </div> ); };
(2)拓展
1 2 3 4 interface ContextState { name: string | null ; } const Context = React.createContext({} as ContextState);
8、ref相关 (1)ref 1 2 3 4 5 6 7 class CssThemeProvider extends React .PureComponent <Props > { private rootRef = React.createRef<HTMLDivElement>(); render ( ) { return <div ref ={this.rootRef} > {this.props.children}</div > ; } }
(2)forwardRef 1 2 3 4 5 6 7 type Props = { children : React.ReactNode; type: "submit" | "button" }; export type Ref = HTMLButtonElement;export const FancyButton = React.forwardRef<Ref, Props>((props, ref ) => ( <button ref={ref} className="MyClassName" type={props.type}> {props.children} </button> ));
9、Portals (1)类组件Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const modalRoot = document .getElementById("modal-root" ) as HTMLElement;export class Modal extends React .Component { el: HTMLElement = document .createElement("div" ); componentDidMount ( ) { modalRoot.appendChild(this .el); } componentWillUnmount ( ) { modalRoot.removeChild(this .el); } render ( ) { return ReactDOM.createPortal(this .props.children, this .el); } }
(2)函数组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React, { useEffect, useRef } from "react" ;import { createPortal } from "react-dom" ;const modalRoot = document .querySelector("#modal-root" ) as HTMLElement;const Modal: React.FC<{}> = ({ children } ) => { const el = useRef(document .createElement("div" )); useEffect(() => { const current = el.current; modalRoot!.appendChild(current); return () => void modalRoot!.removeChild(current); }, []); return createPortal(children, el.current); }; export default Modal;
10、错误处理(Error Boundaries) (1)什么是Error Boundaries
部分UI组件中的JavaScript错误不应该破坏整个应用程序。为了解决React用户的这个问题,React 16引入了一个新的概念Error Boundaries
Error Boundaries是整个react组件去捕捉其所有子组件中的js错误,并且将其记录,用一个回退的组件去显示出来,而不是使整个的react组件树崩溃。
增加Error boundaries能够增加更好的用户体验,如果你的组件中某个组件发生错误,但是这个发生错误的组件并不会去影响其他的组件中的交互功能。
(2)使用react-error-boundary库 (3)自定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import React, { Component, ErrorInfo, ReactNode } from "react" ;interface Props { children: ReactNode; } interface State { hasError: boolean; } class ErrorBoundary extends Component <Props , State > { public state: State = { hasError: false }; public static getDerivedStateFromError(_: Error ): State { return { hasError : true }; } public componentDidCatch (error: Error , errorInfo: ErrorInfo ) { console .error("Uncaught error:" , error, errorInfo); } public render ( ) { if (this .state.hasError) { return <h1 > Sorry.. there was an error</h1 > ; } return this .props.children; } } export default ErrorBoundary;
11、Concurrent React/React Suspense(异步加载) 暂无
(三)故障排除手册(Troubleshooting Handbook) 1、类型错误(Types)
(1)联合类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class App extends React .Component < {}, { count: number | null ; } > { state = { count: null , }; render ( ) { return <div onClick ={() => this.increment(1)}>{this.state.count}</div > ; } increment = (amt: number ) => { this .setState((state ) => ({ count: (state.count || 0 ) + amt, })); }; }
(2)类型守护(Type Guarding) 有时联合类型A|B中A和B都是object时,代表A或者B或者两者兼有。如果你期望时A类型的时候,会引起一些混乱。使用in方法、自定义类型守卫进行判断 https://zhuanlan.zhihu.com/p/108856165
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 interface Admin { role: string; } interface User { email: string; } function redirect (user: Admin | User ) { if ("role" in user) { routeToAdminPage(user.role); } else { routeToHomePage(user.email); } } function isAdmin (user: Admin | User ): user is Admin { return (user as any).role !== undefined ; }
(3)可选类型 1 2 3 4 5 6 7 8 9 class MyComponent extends React .Component < { message?: string; }> { render ( ) { const { message = "default" } = this .props; return <div > {message}</div > ; } }
(4)枚举类型Enum
1 export declare type Position = "left" | "right" | "top" | "bottom" ;
1 2 3 4 5 6 7 8 9 10 export enum ButtonSizes { default = "default" , small = "small" , large = "large" , } export const PrimaryButton = ( props: Props & React.HTMLProps<HTMLButtonElement> ) => <Button size ={ButtonSizes.default} {...props } /> ;
(5)类型断言 有时候作为代码编写者你比ts解析器了解代码,确定当前变量类型比ts解析器认为的更狭窄(narrower)。可以使用as关键字进行断言(类似数据类型强制转换)
1 2 3 4 5 6 7 8 9 10 class MyComponent extends React .Component < { message: string; }> { render ( ) { const { message } = this .props; return ( <Component2 message={message as SpecialMessageType}>{message}</Component2> ); } }
(6)两个接口相同的结构,如何区分
使用brand关键字,并且设置其值为unique symbol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type OrderID = string & { readonly brand: unique symbol }; type UserID = string & { readonly brand: unique symbol }; type ID = OrderID | UserID; function OrderID (id: string ) { return id as OrderID; } function UserID (id: string ) { return id as UserID; } function queryForUser (id: UserID ) { } queryForUser(OrderID("foobar" ));
(7)typeof获取实例的类型 1 2 3 4 5 6 7 8 const [state, setState] = React.useState({ foo: 1 , bar: 2 , }); const someMethod = (obj: typeof state ) => { setState(obj); };
(8)Partial使用(合并对象时常用) 1 2 3 4 5 6 7 8 9 const [state, setState] = React.useState({ foo: 1 , bar: 2 , }); const partialStateUpdate = (obj: Partial<typeof state> ) => setState({ ...state, ...obj }); partialStateUpdate({ foo : 2 });
(9)使用的类型react没有导出 有些可以手动获得
1 2 3 4 5 6 7 import { Button } from "library" ;type ButtonProps = React.ComponentProps<typeof Button>; type AlertButtonProps = Omit<ButtonProps, "onClick" >; const AlertButton: React.FC<AlertButtonProps> = (props ) => ( <Button onClick={() => alert("hello" )} {...props} /> );
使用ReturnType获取函数返回对象的类型
1 2 3 4 5 function foo (bar: string ) { return { baz : 1 }; } type FooReturn = ReturnType<typeof foo>;
二、 高阶组件(HOC) todo
三、进阶 todo
四、向ts迁移 todo