背景
React是前端编写组件的方式, Typescript为组件提供了强类型的类型提示和检查, 尤其是对于组件属性类型的提示, 可以极大帮助组件的使用者快速准确的提供属性值.
因此极力推荐使用Typescript编写React组件.
如何在React中优雅的使用Typescript
在React使用Typescript主要集中在两个方面:
- 如何使用Typescript编写和使用React组件
- 如何使用Typescript引用React库自身的API
在深入介绍之前, 有必要对React自带的常用Typescript类型做一介绍.
React中的类型
React.JSX.Element vs React.ReactNode
-
React.JSX.Element
是 React.createElement()方法的返回值的类型, 也就是所有
React元素的类型 -
React.ReactNode
是 React 可以渲染的任何东西的类型(React.JSX.Element, number, string, boolean, bigint, symbol, null or undefined)
注意: 对象不是合法的组件返回值.
React.ElementType vs React.ComponentType
- React.ElementType 所有组件的类型, 包括原生组件,类组件以及函数组件的类型
- React.ComponentType 类组件和函数组件的类型, 不含原生组件
Interface vs Type alias
- React 组件的属性使用 Type alias, 因为 Type alias 有更多的约束以便实现一致性.
- 公共 API 定义使用 Interface, 以便 API 使用者通过声明合并扩展它的定义.
React 组件属性类型
通常, React 组件渲染的根组件是React的原生组件, 此时, 组件属性可以分为自身的属性和需要传递给原生组件的属性.
最佳实践: 将组件自身属性和继承属性分开定义. 这样做的好处是: 当其它组件需要基于基础组件的属性扩展时, 只需继承基础组件的自身属性即可而不必继承原生组件的属性, 因为扩展的组件可能会使用不同的根组件.
看一个例子:
export type MyComponentOwnProps = {
defaultValue?: string;
value?: string;
onChange?: (val: string) => void;
}
type MyComponentProps = MyComponentOwnProps &
Omit<React.ComponentPropsWithoutRef<"div">, keyof MyComponentOwnProps>;
export const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>( (props, ref) => {
// ...
return (
<div>
//...
</div>
);
});
该例子中只导出了自身属性, 合并后的最终属性则作为私有属性不导出.
这里也使用了一个技巧: Omit<React.ComponentPropsWithoutRef<"div">, keyof MyComponentOwnProps> 会自动排除掉组掉自身属性与原生组件属性同名的属性.
这个例子可以作为编写组件的模板使用.
单个属性的类型
export interface MyComponentProps {
message: string;
count: number;
disabled: boolean;
/** 类型化的数组 */
names: string[];
/** 字符串字面量枚举 */
status: "waiting" | "success";
/** 类型化的对象 */
obj: {
id: string;
title: string;
};
/** 类型化的对象数组 */
objArr: {
id: string;
title: string;
}[];
/** 任何非原生类型,但不可访问任何属性 (不常用) */
obj2: object;
/** 与上面等价 (不常用)*/
obj3: {};
/** 带有任意属性的对象 */
dict1: {
[key: string]: MyTypeHere;
};
/** 与上面的等价 */
dict2: Record<string, MyTypeHere>;
/** 无参数无返回值的函数, 如自定义的事件处理器 */
onClick: () => void;
/** 带参数无返回值的函数, 如自定义的事件处理器 */
onChange: (id: number) => void;
/** 带参数无返回值的函数, 如原生的事件处理器 */
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** 带参数无返回值的函数, 如原生的事件处理器 */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
/** 可选属性
*
* @default a
*/
optional?: OptionalType;
/** `useState()`返回的设置函数作为属性类型 */
setState: React.Dispatch<React.SetStateAction<MyTypeHere>>;
/** useReducer()返回的dispatch作为属性类型 */
dispatch: React.Dispatch<MyTypeHere>;
/** 组件类型 */
as: React.ElementType<MyTypeHere>;
/** 从其它组件的单个属性中扩展 */
slots?: OtherComponentOwnProps["slots"] & {
dropdown?: string;
};
/** style属性 */
style: React.CSSProperties; // 可以传递给style属性.
/** children属性 */
children: React.ReactNode; // 可被React渲染的任何单个东西
children: [React.ReactNode, React.ReactNode]; // 孩子为指定个数的元组
children: React.ReactNode[]; // 可被React渲染的任何东西的数组
children: React.JSX.Element; // 单个React组件元素, 不能是true,false,null,undefined,字符串或其它非组件
children: [React.JSX.Element, React.JSX.Element]; // 孩子为指定个数的元组
children: React.JSX.Element[]; // React元素数组
children: (foo: string) => ReactNode; // 函数作为孩子
}
附属组件
有时, 想在主组件上带附属组件
首先按照按上面的规则定义, 但组件名使用_作为后缀(这只是作为约定), 但不需要到导出, 在文件最后添加:
如果想命名导出:
import { Other } from "./Other
// ...
export const MyComponent = Object.assign(MyComponent_, { Other });
如果想默认导出:
import { Other } from "./Other
// ...
export default Object.assign(Grid_, { Col });
泛型组件
import { ReactNode, useState } from "react";
interface Props<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}
function List<T>(props: Props<T>) {
const { items, renderItem } = props;
const [state, setState] = useState<T[]>([]); // You can use type T in List function scope.
return (
<div>
{items.map(renderItem)}
<button onClick={() => setState(items)}>Clone</button>
{JSON.stringify(state, null, 2)}
</div>
);
}
使用时会进行自动类型推断:
ReactDOM.render(
<List
items={[1, 2]} // 会根据items的属性值推断T为number
renderItem={(item) => (
<li key={item}>
{/* Error: Property 'toPrecision' does not exist on type 'string'. */}
{item.toPrecision(3)}
</li>
)}
/>,
document.body
);
也可以在 TSX 中明确指定 T 类型
ReactDOM.render(
<List<number>
items={["a", "b"]} // Error: Type 'string' is not assignable to type 'number'.
renderItem={(item) => <li key={item}>{item.toPrecision(3)}</li>}
/>,
document.body
);
React自身的API的引用
React Hooks
useState
不可空的基本类型:
const [val, setVal] = React.useState(false);
val 为 boolean类型, setVal 为只接受一个boolean参数的void函数.
初始值为空的对象类型
const [user, setUser] = React.useState<IUser | null>(null);
useReducer
可以使用可辨识联合类型定义 actions, 方便在 reducer 中的 switch 判断.
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();
}
}
useRef
React 中 useRef 返回的引用可以是只读的,也可以是可变的. 即便是只读的, 也需要判断是否为空. 因为有可能你使用了条件渲染, 而 ref 的元素可能不一定渲染, 或者忘了赋值 ref.
const divRef = useRef<HTMLDivElement>(null); // 返回值类型为: RefObject, 它是只读. 只能赋值给ref属性. 如果你不知道元素的类型, 一旦你赋值给了ref, VsCode会提示你所需的类型.
const intervalRef = useRef<number | null>(null); // 返回值类型为: MutableRefObject, 它是可变的. 可能为空, 代码必须判断是否为空, 用于组件内的自定义缓存.
const intervalRef = useRef<HTMLDivElement>(null!); // 返回值类型为: MutableRefObject, 它是可变的. 强制断言为不为空, 代码可以不必判断,只要确保 divRef.current不为null. 用于组件内的自定义缓存.
const intervalRef = useRef(1); // 返回值类型为: MutableRefObject, 它是可变的. 自动推断不可为空, 用于组件内的自定义缓存. 需要确保 divRef.current不为null
React Context
const StoreContext = React.createContext<StoreContextState | undefined>(
undefined
);
export function useStore() {
const store = React.useContext(StoreContext);
if (store === undefined) throw new Error("useStore must be inside a Provider with a value");
return store;
}