一、前言
在React项目开发中,写出优雅的、更有意义的typescript代码,是我们一直追求的。本文广东靓仔带小伙伴们一起来看看React项目实际开发中用到的一些常用类型示例。
二、基本prop类型示例
常规的程序中使用的 TypeScript 类型列表:
type AppProps = { message: string; count: number; disabled: boolean; /** 一个类型的数组!*/ names: string[]; /** 用于指定精确字符串值的字符串文字,使用联合类型将它们连接在一起 */ status: "waiting" | "success"; /** 任何对象,只要你不使用它的属性(不常见,但用作占位符)*/ obj: object; obj2: {}; // 和 `object` 差不多,和 `Object` 完全一样 /** 具有任意数量属性的对象 (PREFERRED) */ obj3: { id: string; title: string; }; /** 对象数组!(常见的) */ objArr: { id: string; title: string; }[]; /** 具有任意数量的相同类型属性的 dict 对象 */ dict1: { [key: string]: MyTypeHere; }; dict2: Record<string, MyTypeHere>; // 相当于 dict1 /** 任何函数,只要你不调用它(不推荐) */ onSomething: Function; /** 不接受或不返回任何内容的函数(非常常见) */ onClick: () => void; /** 带有命名props的函数(非常常见) */ onChange: (id: number) => void; /** 接受事件的函数类型语法(非常常见) */ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; /** 接受事件的替代函数类型语法(非常常见) */ onClick(event: React.MouseEvent<HTMLButtonElement>): void; /** 一个可选的props(非常常见!) */ optional?: OptionalType; };
三、有用的 React Prop 类型示例
export declare interface AppProps { children?: React.ReactNode; // 最好,接受 React 可以渲染的所有内容 childrenElement: JSX.Element; // 单个 React 元素 style?: React.CSSProperties; // 传递样式props onChange?: React.FormEventHandler<HTMLInputElement>; // 形成事件!泛型参数是 event.target 的类型 props: Props & React.ComponentPropsWithoutRef<"button">; // 模拟按钮元素的所有 props 并明确不转发其 ref props2: Props & React.ComponentPropsWithRef<MyButtonWithForwardRef>; // 模拟 MyButtonForwardedRef 的所有 props 并显式转发其 ref }
type还是interface?
这是一个有用的经验法则:
在创作库或第 3 方环境类型定义时,始终用于公共 API 的定义,因为这允许使用者在缺少某些定义时通过声明合并来扩展它们。
考虑为您的 React 组件 Props 和 State 使用,以保持一致性并且因为它受到更多限制。
四、函数组件
这些可以写成普通函数,接受一个props参数并返回一个 JSX 元素。
type AppProps = { message: string; }; /* 如果导出使用 `interface` 以便消费者可以扩展 */ // 声明函数组件的最简单方法;推断返回类型。 const App = ({ message }: AppProps) => <div>{message}</div>; // 您可以选择注释返回类型,这样如果您不小心返回了其他类型,则会引发错误 const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>; // 你也可以内联类型声明;消除了命名props类型,但看起来重复 const App = ({ message }: { message: string }) => <div>{message}</div>;
hook
useState类型推断对于简单值非常有效:
const [state, setState] = useState(false); // `state` 被推断为布尔值 // `setState` 只接受布尔值
许多钩子都是用 null-ish 默认值初始化的,你可能想知道如何提供类型。
显式声明类型,并使用联合类型:
const [user, setUser] = useState<User | null>(null); setUser(newUser);
如果状态在设置后不久初始化并且始终具有以下值,还可以使用类型断言:
const [user, setUser] = useState<User>({} as User); setUser(newUser);
useReducer您可以将有区别的联合用于 reducer 操作。不要忘记定义reducer的返回类型,否则TypeScript会推断出来。
import { useReducer } from "react"; 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] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "decrement", payload: "5" })}> - </button> <button onClick={() => dispatch({ type: "increment", payload: 5 })}> + </button> </> ); }
useEffect / useLayoutEffect
useEffect和都useLayoutEffect用于执行副作用并返回一个可选的清理函数,这意味着如果它们不处理返回值,则不需要类型。
使用 时useEffect,注意不要返回除函数 or 以外的任何东西undefined,否则 TypeScript 和 React 都会提示你。
这在使用箭头函数时可能很微妙:
function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; useEffect( () => setTimeout(() => { /* do stuff */ }, timerMs), [timerMs] ); // 反面例子!setTimeout 隐式返回一个数字 // 因为箭头函数体没有用大括号括起来 return null; }
useRef
在 TypeScript 中,返回一个只读或可变useRef的引用,取决于您的类型参数是否完全覆盖初始值。选择一个适合您的用例。
1、DOM 元素 ref 访问 DOM 元素:
仅提供元素类型作为参数,并null用作初始值。.current在这种情况下,返回的引用将具有由 React 管理的只读引用TypeScript 期望将此 ref 提供给元素的ref prop:
function Foo() { // - 如果可能,请尽可能具体。例如,HTMLDivElement // 比 HTMLElement 好,也比 Element 好得多。 // - 从技术上讲,这会返回 RefObject<HTMLDivElement> const divRef = useRef<HTMLDivElement>(null); useEffect(() => { // 注意 ref.current 可能为空。这是意料之中的 // 有条件地渲染被引用的元素,或者你可能忘记分配它 if (!divRef.current) throw Error("divRef is not assigned"); // 现在 divRef.current 肯定是 HTMLDivElement doSomethingWith(divRef.current); }); // 将 ref 赋予一个元素,以便 React 可以管理它 return <div ref={divRef}>etc</div>; }
如果确定divRef.current永远不会为空,也可以使用非空断言运算符!:
const divRef = useRef<HTMLDivElement>(null!); // 无需检查是否为空 doSomethingWith(divRef.current);
2、可变值 ref 要具有可变值:提供您想要的类型,并确保初始值完全属于该类型:
function Foo() { // 从技术上讲,这将返回 MutableRefObject<number | 空> const intervalRef = useRef<number | null>(null); // 你自己管理 ref(这就是为什么它被称为 MutableRefObject!) useEffect(() => { intervalRef.current = setInterval(...); return () => clearInterval(intervalRef.current); }, []); // ref 不会传递给任何元素的 "ref" 属性 return <button onClick={/* clearInterval the ref */}>Cancel timer</button>; }
自定义hook如果你在自定义 Hook 中返回一个数组,你会想要避免类型推断,因为 TypeScript 会推断一个联合类型(当你实际上想要在数组的每个位置使用不同的类型时)
import { useState } from "react"; export function useLoading() { const [isLoading, setState] = useState(false); const load = (aPromise: Promise<any>) => { setState(true); return aPromise.finally(() => setState(false)); }; return [isLoading, load] as const; // 推断 [boolean, typeof load] 而不是 (boolean | typeof load)[] }
五、类组件
在TypeScript 中,React.Component是一个泛型类型(aka React.Component),因此希望为它提供(可选)prop 和 state 类型参数:
type MyProps = { // 使用 `interface` 也可以 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> ); } }
Tips: 可以导出/导入/扩展这些类型/接口以供重用。类方法:像往常一样做,要记住函数的任何参数也需要输入:
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, })); }; }
类属性:如果需要声明类属性以供以后使用,只需将其声明为state,但无需赋值:
class App extends React.Component<{ message: string; }> { pointer: number; // 像这样 componentDidMount() { this.pointer = 3; } render() { return ( <div> {this.props.message} and {this.pointer} </div> ); } }
getDerivedStateFromProps
派生状态可以使用钩子来实现,这也可以帮助设置memoization。
以下是可以注释的几种方法getDerivedStateFromProps
1、如果已显式键入派生状态并希望确保 from 的返回值getDerivedStateFromProps符合它。
class Comp extends React.Component<Props, State> { static getDerivedStateFromProps( props: Props, state: State ): Partial<State> | null { // } }
2、希望函数的返回值确定的状态时。
class Comp extends React.Component< Props, ReturnType<typeof Comp["getDerivedStateFromProps"]> > { static getDerivedStateFromProps(props: Props) {} }
3、想要具有其他状态字段和记忆的派生状态时
type CustomValue = any; interface Props { propA: CustomValue; } interface DefinedState { otherStateField: string; } type State = DefinedState & ReturnType<typeof transformPropsToState>; function transformPropsToState(props: Props) { return { savedPropA: props.propA, // 保存以备memoization derivedState: props.propA, }; } class Comp extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); this.state = { otherStateField: "123", ...transformPropsToState(props), }; } static getDerivedStateFromProps(props: Props, state: State) { if (isEqual(props.propA, state.savedPropA)) return null; return transformPropsToState(props); } }
六、form和event
如果需要单独定义事件处理程序,IDE 工具在这里真的很方便,因为 @type 定义带有丰富的类型。输入要查找的内容,通常自动完成功能会为您提供帮助。onChange这是表单事件的样子
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 }); }; render() { return ( <div> <input type="text" value={this.state.text} onChange={this.onChange} /> </div> ); } }
React.FormEvent<>除了使用and键入参数和返回值void,您还可以将类型应用于事件处理程序本身
// 在 = 的左侧输入 onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { this.setState({text: e.currentTarget.value}) }
键入 onSubmit,在表单中包含不受控制的组件
如果不太关心事件的类型,可以使用 React.SyntheticEvent。
如果目标表单具有想要访问的自定义命名输入,可以使用类型断言:
<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>
事件类型列表
七、Context
基本示例
import { createContext } from "react"; interface AppContextInterface { name: string; author: string; url: string; } const AppCtx = 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> ); // 在你的应用中使用 import { useContext } from "react"; export const PostInfo = () => { const appContext = useContext(AppCtx); return ( <div> Name: {appContext.name}, Author: {appContext.author}, Url:{" "} {appContext.url} </div> ); };
扩展示例使用createContext空对象作为默认值
interface ContextState { // 使用上下文设置你想要处理的状态类型,例如 name: string | null; } // 设置一个空对象为默认状态 const Context = createContext({} as ContextState); // 像在 JavaScript 中一样设置上下文提供程序
使用createContext 和 context getters来制作 a createCtx with no ,但无需检查:
import { createContext, useContext } from "react"; const currentUserContext = createContext<string | undefined>(undefined); function EnthusasticGreeting() { const currentUser = useContext(currentUserContext); return <div>HELLO {currentUser!.toUpperCase()}!</div>; } function App() { return ( <currentUserContext.Provider value="Anders"> <EnthusasticGreeting /> </currentUserContext.Provider> ); }
注意我们需要的显式类型参数,因为我们没有默认string值:
const currentUserContext = createContext<string | undefined>(undefined); // ^^^^^^^^^^^^^^^^^^
连同非空断言告诉 TypeScript currentUser肯定会在那里:
return <div>HELLO {currentUser!.toUpperCase()}!</div>; //
这是不幸的,因为我们知道稍后在我们的应用程序中,a Provider将填充上下文。
有几个解决方案:
1、可以通过断言非空来解决这个问题:
const currentUserContext = createContext<string>(undefined!);
2、我们可以编写一个名为的辅助函数createCtx来防止访问Context未提供值的 a。通过这样做,API 相反,我们不必提供默认值,也不必检查:
import { createContext, useContext } from "react"; /** * 创建上下文和提供者的助手,没有预先的默认值,并且 * 无需一直检查未定义。 */ function createCtx<A extends {} | null>() { const ctx = createContext<A | undefined>(undefined); function useCtx() { const c = useContext(ctx); if (c === undefined) throw new Error("useCtx must be inside a Provider with a value"); return c; } return [useCtx, ctx.Provider] as const; // 'as const' 使 TypeScript 推断出一个元组 } // 用法: // 我们仍然需要指定一个类型,但没有默认值! export const [useCurrentUserName, CurrentUserProvider] = createCtx<string>(); function EnthusasticGreeting() { const currentUser = useCurrentUserName(); return <div>HELLO {currentUser.toUpperCase()}!</div>; } function App() { return ( <CurrentUserProvider value="Anders"> <EnthusasticGreeting /> </CurrentUserProvider> ); }
3、可以更进一步,使用createContext和context getters结合这个想法。
import { createContext, useContext } from "react"; /** * 创建上下文和提供者的助手,没有预先的默认值,并且 * 无需一直检查未定义。 */ function createCtx<A extends {} | null>() { const ctx = createContext<A | undefined>(undefined); function useCtx() { const c = useContext(ctx); if (c === undefined) throw new Error("useCtx must be inside a Provider with a value"); return c; } return [useCtx, ctx.Provider] as const; // 'as const' 使 TypeScript 推断出一个元组 } // 用法 export const [useCtx, SettingProvider] = createCtx<string>(); // 指定类型,但不需要预先指定值 export function App() { const key = useCustomHook("key"); // 从钩子中获取值,必须在组件中 return ( <SettingProvider value={key}> <Component /> </SettingProvider> ); } export function Component() { const key = useCtx(); // 仍然可以在没有空检查的情况下使用! return <div>{key}</div>; }
4、使用createContext and useContext制作一个createCtx with unstated-like 上下文设置器:
import { createContext, Dispatch, PropsWithChildren, SetStateAction, useState, } from "react"; export function createCtx<A>(defaultValue: A) { type UpdateType = Dispatch<SetStateAction<typeof defaultValue>>; const defaultUpdate: UpdateType = () => defaultValue; const ctx = createContext({ state: defaultValue, update: defaultUpdate, }); function Provider(props: PropsWithChildren<{}>) { const [state, update] = useState(defaultValue); return <ctx.Provider value={{ state, update }} {...props} />; } return [ctx, Provider] as const; // 或者,[typeof ctx, typeof Provider] } // 用法 import { useContext } from "react"; const [ctx, TextProvider] = createCtx("someText"); export const TextContext = ctx; export function App() { return ( <TextProvider> <Component /> </TextProvider> ); } export function Component() { const { state, update } = useContext(TextContext); return ( <label> {state} <input type="text" onChange={(e) => update(e.target.value)} /> </label> ); }