React 中使用 TS
第一章:
第二章:组件类型注解
一、函数式组件
1. Props 的类型注释
1)使用 React.FC
React.FC<P> 是函数式组件在 TypeScript 使用的一个泛型,FC 就是 FunctionComponent 的缩写,事实上 React.FC 可以写成 React.FunctionComponent。
来看下内部 TS 如何声明的:
// 函数组件,其也是类型别名
type FC<P = {}> = FunctionComponent<P>;
// FunctionComponent<T> 是一个接口,里面包含其函数定义和对应返回的属性
interface FunctionComponent<P = {}> {
// 接口可以表示函数类型,通过给接口定义一个调用签名实现
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}例子:
// 1. 定义 Props 接口
interface UserCardProps {
children?: React.ReactNode; // 显式定义 children
functionChildren: (name: string) => React.ReactNode; // 返回 React 节点
style?: React.CSSProperties; // React style
onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型
}
// 2. 泛型传入 Props
const UserCard: React.FC<UserCardProps> = ({ name, children }) => {
return <div>{name}</div>;
};defaultProps 不需要了,可以使用对象的解构默认值即可。
2)直接解构参数注解 (推荐)
告诉组件它接收什么参数。通常使用 interface 或 type 来定义。
注意:在 React 18 中, children 不再默认包含在 React.FC 中,如果你的组件包含子元素,必须显式定义。
import React from 'react';
const UserCard = ({ children }: UserCardProps) => {
return (
<div>
<h1>{name}</h1>
{children}
</div>
);
};2. Hooks 的类型注释
1)useState
通常不需要写,TS 会自动推断。
const [count, setCount] = useState(0);
// count 被推断为 number 类型
// setCount 只能处理 number 类型复杂类型 / 联合类型等,需要使用泛型 <>。
interface User {
id: number;
name: string;
}
// 初始值为 null,但将来是 User 对象
const [user, setUser] = useState<User | null>(null);
/*
使用时应该这样书写:
user && user.name
user?.name
*/
// 初始值为空数组,但将来装的是 User
const [list, setList] = useState<User[]>([]);2)useEffect
useEffect() 本身不需要、也不支持像 useState<T> 这样传递泛型参数。但有两个非常关键的类型规则必须遵守,否则 TS 会报错:
回调函数不能是 async 的。
tsx// ❌ TS 报错:类型 "Promise<void>" 不能赋值给类型 "void | Destructor" useEffect(async () => { const data = await fetchApi(); setData(data); }, []); // -------------------------------------------------------------------------- // ✅ 正确写法 useEffect(() => { // 1. 在内部定义异步函数 const fetchData = async () => { try { const result = await fetchApi(); setData(result); } catch (error) { console.error(error); } }; // 2. 调用它 fetchData(); // 3. (可选) 返回清除函数,必须是同步的 return () => { console.log('Cleanup'); }; }, []);清除函数 (Cleanup) 的类型
如果在 useEffect 中有返回值,TypeScript 会强制要求它必须是一个同步的、且返回 void 的函数
() => void | undefined。tsxuseEffect(() => { const timer = setInterval(() => { console.log('Tick'); }, 1000); // ✅ 正确:返回一个函数 return () => { clearInterval(timer); }; // ❌ 错误:直接返回了 undefined 以外的值(比如返回了 timer ID) // return timer; }, []);
3)useRef
// 初始值通常为 null
const ref1 = useRef<HTMLInputElement>(null);
const ref2 = useRef<HTMLElement>(null!);
const ref3 = useRef<HTMLElement | null>(null);
const focusInput = () => {
// 使用时需要判断是否为 null (?. 操作符)
ref1.current?.focus();
};
return <input ref={ref1} />;4)useContext
useContext() 的类型推断是自动的,前提是必须在创建 Context 时(即调用 createContext() 时)正确传入泛型。
不需要写 useContext<MyType>(MyContext),而是应该专注于 createContext()。
场景一:初始值为 null(最严谨,推荐)
适用于无法在创建 Context 时提供一个完整的默认值(比如数据需要等接口请求回来)。
// 1. 定义 Context 的数据形状
interface UserContextType {
name: string;
login: () => void;
}
// 2. 创建 Context
// 注意:这里泛型写 <UserContextType | null>,初始值传 null
const UserContext = createContext<UserContextType | null>(null);
// ----------------------- 组件中使用 -----------------------
const UserProfile = () => {
// 3. 这里 user 的类型会自动推断为: UserContextType | null
const user = useContext(UserContext);
// 4. 必须做非空检查 (Type Guard)
if (!user) {
throw new Error("UserProfile must be used within a UserProvider");
}
return (
<div>
<h1>{user.name}</h1>
<button onClick={user.login}>Login</button>
</div>
);
};场景二:使用类型断言
适用于确定你的组件一定会被包裹在 Provider 里面,不想每次使用都判断 if (!user)。
import React, { createContext, useContext } from 'react';
interface ThemeContextType {
color: string;
toggleTheme: () => void;
}
// 1. 创建 Context
// 技巧:使用 {} as ThemeContextType
// 这样 TS 认为它有值,但实际上初始值是空的(运行时如果没 Provider 会报错,但开发时方便)
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType);
// ----------------------- 组件中使用 -----------------------
const ThemeButton = () => {
// 2. 这里 theme 直接就是 ThemeContextType,不需要判空
const theme = useContext(ThemeContext);
return (
<button style={{ color: theme.color }} onClick={theme.toggleTheme}>
Toggle
</button>
);
};3. 自定义 Hooks
Hooks 的美妙之处不只有减小代码行的功效,重点在于能够做到逻辑与 UI 分离。做纯粹的逻辑层复用。
例子:当你自定义 Hooks 时,返回的数组中的元素是确定的类型,而不是联合类型。可以使用 const-assertions 。
export function useLoading() {
const [isLoading, setIsLoading] = useState(false);
const load = (aPromise: Promise<any>) => {
setIsLoading(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联合类型 (boolean | typeof load)[]
}也可以断言成元组类型。
export function useLoading() {
const [isLoading, setIsLoading] = useState(false);
const load = (aPromise: Promise<any>) => {
setIsLoading(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>
];
}如果对这种需求比较多,每个都写一遍比较麻烦,可以利用泛型定义一个辅助函数,且利用 TS 自动推断能力。
function tuplify<T extends any[]>(...elements: T) {
return elements;
}
// -------------------------- 使用 --------------------------
function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // (number | (() => void))[]
}
function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return tuplify(numberValue, functionValue); // [number, () => void]
}二、React.Component<P, S>
React.Component< P, S > 是定义 class 组件的一个泛型,第一个参数是 props、第二个参数是 state。
来看下内部 TS 如何声明的:
import React from "react";
// props 的接口
interface demo2PropsInterface {
props1: string
};
// state 的接口
interface demo2StateInterface {
state1: string
};
class Demo2 extends React.Component<demo2PropsInterface, demo2StateInterface> {
constructor(props: demo2PropsInterface) {
super(props);
this.state = {
state1: 'state1'
}
}
render() {
return (
<div>{this.state.state1 + this.props.props1}</div>
);
}
}
export default Demo2;三、React.Reducer<S, A>
useState 的替代方案,接收一个形如 (state, action) => newState 的 reducer,并返回当前 state 以及其配套的 dispatch 方法。语法:const [state, dispatch] = useReducer(reducer, initialArg, init);
import React, { useReducer, useContext } from "react";
interface stateInterface {
count: number
};
interface actionInterface {
type: string,
data: {
[propName: string]: any
}
};
const initialState = {
count: 0
};
// React.Reducer 其实是类型别名,其实质上是 `type Reducer<S, A> = (prevState: S, action: A) => S;`
// 因为reducer是一个函数,其接受两个泛型参数S和A,返回S类型
const reducer: React.Reducer<stateInterface, actionInterface> = (state, action) => {
const {type, data} = action;
switch (type) {
case 'increment': {
return {
...state,
count: state.count + data.count
};
}
case 'decrement': {
return {
...state,
count: state.count - data.count
};
}
default: {
return state;
}
}
}四、React.Context<T>
1. React.createContext
其会创建一个 Context 对象,当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
注:只要当组件所处的树没有匹配到 Provider 时,其 defaultValue 参数参会生效。
const MyContext = React.createContext(defaultValue);
const Demo = () => {
return (
// 注:每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化。
<MyContext.Provider value={xxxxxx}>
// ……
</MyContext.Provider>
);- useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。语法如下所示:
const value = useContext(MyContext);
import React, {useContext} from "react";
const MyContext = React.createContext('');
const Demo3Child: React.FC<{}> = () => {
const context = useContext(MyContext);
return (
<div>
{context}
</div>
);
}
const Demo3: React.FC<{}> = () => {
return (
<MyContext.Provider value={'222222'}>
<MyContext.Provider value={'33333'}>
<Demo3Child />
</MyContext.Provider>
</MyContext.Provider>
);
};- 使用
import React, {useReducer, useContext} from "react";
interface stateInterface {
count: number
};
interface actionInterface {
type: string,
data: {
[propName: string]: any
}
};
const initialState = {
count: 0
};
const reducer: React.Reducer<stateInterface, actionInterface> = (state, action) => {
const {type, data} = action;
switch (type) {
case 'increment': {
return {
...state,
count: state.count + data.count
};
}
case 'decrement': {
return {
...state,
count: state.count - data.count
};
}
default: {
return state;
}
}
}
// React.createContext返回的是一个对象,对象接口用接口表示
// 传入的为泛型参数,作为整个接口的一个参数
// interface Context<T> {
// Provider: Provider<T>;
// Consumer: Consumer<T>;
// displayName?: string | undefined;
// }
const MyContext: React.Context<{
state: stateInterface,
dispatch ?: React.Dispatch<actionInterface>
}> = React.createContext({
state: initialState
});
const Demo3Child: React.FC<{}> = () => {
const {state, dispatch} = useContext(MyContext);
const handleClick = () => {
if (dispatch) {
dispatch({
type: 'increment',
data: {
count: 10
}
})
}
};
return (
<div>
{state.count}
<button onClick={handleClick}>增加</button>
</div>
);
}
const Demo3: React.FC<{}> = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<MyContext.Provider value={{state, dispatch}}>
<Demo3Child />
</MyContext.Provider>
);
};
export default Demo3;