Skip to content

React 中使用 TS

第一章:

第二章:组件类型注解

一、函数式组件

1. Props 的类型注释

1)使用 React.FC

React.FC<P> 是函数式组件在 TypeScript 使用的一个泛型,FC 就是 FunctionComponent 的缩写,事实上 React.FC 可以写成 React.FunctionComponent。

来看下内部 TS 如何声明的:

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;
}

例子:

tsx
// 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 中,如果你的组件包含子元素,必须显式定义。

tsx
import React from 'react';

const UserCard = ({ children }: UserCardProps) => {
  return (
    <div>
      <h1>{name}</h1>
      {children}
    </div>
  );
};

2. Hooks 的类型注释

1)useState

通常不需要写,TS 会自动推断。

tsx
const [count, setCount] = useState(0);
// count 被推断为 number 类型
// setCount 只能处理 number 类型

复杂类型 / 联合类型等,需要使用泛型 <>

tsx
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

    tsx
    useEffect(() => {
      const timer = setInterval(() => {
        console.log('Tick');
      }, 1000);
    
      // ✅ 正确:返回一个函数
      return () => {
        clearInterval(timer);
      };
    
      // ❌ 错误:直接返回了 undefined 以外的值(比如返回了 timer ID)
      // return timer;
    }, []);
3)useRef
tsx
// 初始值通常为 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 时提供一个完整的默认值(比如数据需要等接口请求回来)。

tsx
// 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)

tsx
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 。

typescript
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)[]
}

也可以断言成元组类型。

typescript
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 自动推断能力。

typescript
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 如何声明的:

tsx
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);

tsx
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 参数参会生效。

jsx
const MyContext = React.createContext(defaultValue);
 
const Demo = () => {
  return (
      // 注:每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化。
    <MyContext.Provider value={xxxxxx}>
      // ……
    </MyContext.Provider>
  );
  1. useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。语法如下所示:const value = useContext(MyContext);

jsx
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>
    );
};
  1. 使用
tsx
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;
preview
图片加载中
预览

Released under the MIT License.