作家村上春树在《当我谈跑步时我谈些什么》里说道:“Pain is inevitable,but suffering is optional” 即 苦楚不可避免,但咱们能够抉择不受苦。这个选择权,来自不反抗过来,不担心将来。

大家好,我是柒八九

在前几天,咱们开拓了--TypeScript实战系列,次要讲TSReact中的利用实战。

大家如果对React理解/相熟的话,想必都听过Hook。在当下的React开发中,函数组件大行其道。而Hook就是为了给函数组件增加外部状态还有解决副作用的。换句话说,Hook曾经在当初的React的开发中, 变得不可代替。

而,明天咱们就简略的聊聊,如何利用TSHook进行类型化解决。有一点须要特地指出,对hook进行类型化解决,须要利用泛型的语法,如果对泛型没有一个大体的理解,还是须要异步一些惯例材料中,先进行简略的学习。

  • TS_React:应用泛型来改善类型
  • typescriptlang_generics

好了,天不早了。咱们开始粗发

你能所学到的知识点

React各种hook的类型化解决,总有一款,让你骑虎难下

文章概要

  1. 依赖类型推断
  2. 类型化 useState
  3. 类型化 useReducer
  4. 类型化 useRef
  5. 类型化 forwardRef
  6. 类型化 useEffect 和 useLayoutEffect
  7. 类型化 useMemo 和 useCallback
  8. 类型化 useContext
  9. 类型化自定义hook

1. 依赖类型推断

在绝大部分,TS都能够依据hook中的值来推断它们的类型:也就是咱们常说的类型推断

何为类型推断,简略来说:类型推断就是基于赋值表达式推断类型的能⼒ts采纳将类型标注申明放在变量之后(即类型后置)的形式来对变量的类型进行标注。而使⽤类型标注后置的益处就是编译器能够通过代码所在的高低⽂推导其对应的类型,⽆须再申明变量类型。

  • 具备初始化值的变量
  • 默认值的函数参数
  • 函数返回的类型

都能够依据高低⽂推断进去。

例如,上面的代码能够在ts环境中失常运行,且可能通过类型推断推导出name的类型为string类型。

const [name, setName] = useState('前端柒八九');

何时不能依赖类型推断

上面的两种情境下,类型推断有点力不从心

  • ts推断出的类型过于宽松
  • 类型推断谬误

推断出的类型过于宽松

咱们之前的例子--有一个字符串类型的name。然而咱们假如这个name只能有两个预约的值中的一个。

在这种状况下,咱们会心愿name有一个十分具体的类型,例如这个类型。

type Name = '前端柒八九' | '前端工程师' ;

这种类型同时应用联结类型字面类型

在这种状况下,推断的类型过于宽松(是string,而不是咱们想要的2个字符串的特定子集),这种状况下就必须本人指定类型。

const [name, setName] = useState<Name>('前端柒八九');

类型推断谬误

有时,推断的类型是谬误的(或者限制性太强不是你想要的类型)。这种状况常常产生在ReactuseState 默认值中。比方说,name 的初始值是null

const [name, setName] = useState(null);

在这种状况下,TypeScript 会推断出namenull类型的(这意味着它总是null)。这显然是谬误的:咱们当前会想把 name 设置成一个字符串。

此时你必须通知 TypeScript,它能够是别的类型。

const [name, setName] = useState<string | null>(null);

通过这样解决后,TypeScript 会正确理解name能够是null也能够是string

这里要提到的一件事是,当类型推断不起作用时,应该依附泛型参数而不是类型断言

const [name, setName] = useState<Name>('前端柒八九'); 举荐应用

const [name, setName] = useState('前端柒八九' as Name); 不举荐应用

2. 类型化 useState

在文章结尾,咱们曾经通过类型推断讲过了,如何解决useState的各种状况。这里就不在赘述了。

const [name, setName] = useState<string | null>(null);

3. 类型化 useReducer

useReducer 的类型比 useState 要简单一些。如果看过源码的同学,可能有印象,其实useState就是useReducer简化版

针对useReducer有两样货色要类型化解决:stateaction

这里有一个useReducer的简略例子。针对input做简略的数据收集解决。

import { useReducer } from 'react';const initialValue = {  username: '',  email: '',};const reducer = (state, action) => {  switch (action.type) {    case 'username':      return { ...state, username: action.payload };    case 'email':      return { ...state, email: action.payload };    case 'reset':      return initialValue;    default:      throw new Error(`未定义的action: ${action.type}`);  }};const Form = () => {  const [state, dispatch] = useReducer(reducer, initialValue);  return (    <div>      <input        type="text"        value={state.username}        onChange={(event) =>          dispatch({ type: 'username', payload: event.target.value })        }      />      <input        type="email"        value={state.email}        onChange={(event) =>          dispatch({ type: 'email', payload: event.target.value })        }      />    </div>  );};export default Form;

类型化 reducer 的state

咱们有两个抉择来类型化reducer-state

  • 应用初始值(如果有的话)和 typeof 操作符
  • 应用类型别名

应用typeof 操作符

const initialValue = {  username: '',  email: '',};+ const reducer = (state: typeof initialValue, action) => {    switch (action.type) {      case 'username':        return {...state,  username: action.payload };      case 'email':        return {...state,  email: action.payload };      case 'reset':        return initialValue;      default:        throw new Error(`未定义的action: ${action.type}`);    }};

应用类型别名

+type State = {+  username: string;+  email: string;+};const initialValue = {  username: '',  email: '',};+ const reducer = (state: State, action) => {    switch (action.type) {      case 'username':        return { ...state, username: action.payload };      case 'email':        return { ...state, email: action.payload };      case 'reset':        return initialValue;      default:        throw new Error(`未定义的action: ${action.type}`);    }};

类型化 reducer 的action

reducer-action的类型比reducer-state要难一点,因为它的构造会依据具体的action而扭转。

例如,对于 username-action,咱们可能冀望有以下类型。

type UsernameAction = {  type: 'username';  payload: string;};

但对于 reset-action,咱们不须要payload字段。

type ResetAction = {  type: 'reset';};

咱们能够借助联结类型区别对待不同的action

const initialValue = {  username: "",  email: ""};+type Action =+  | { type: "username"; payload: string }+  | { type: "email"; payload: string }+  | { type: "reset" };+ const reducer = (state: typeof initialValue, action: Action) => {    switch (action.type) {      case "username":        return {...state, username: action.payload };      case "email":        return { ...state, email: action.payload };      case "reset":        return initialValue;      default:        throw new Error(`未定义的action: ${action.type}`);    }};

Action类型示意的是,它能够承受联结类型中蕴含的三种类型中的任何一种。因而,如果 TypeScript 看到 action.typeusername,它就会主动晓得它应该是第一种状况,并且payload应该是一个string

通过对state/action类型化后,useReducer可能从reducer函数的type中推断出它须要的所有。

上面是整体的代码。(省略了,jsx局部)

import { useReducer } from 'react';const initialValue = {  username: '',  email: '',};+type Action =+  | { type: 'username'; payload: string }+  | { type: 'email'; payload: string }+  | { type: 'reset' };+const reducer = (state: typeof initialValue, action: Action) => {  switch (action.type) {    case 'username':      return { ...state, username: action.payload };    case 'email':      return { ...state, email: action.payload };    case 'reset':      return initialValue;    default:      throw new Error(`Unknown action type: ${action.type}`);  }};const Form = () => {  const [state, dispatch] = useReducer(reducer, initialValue);  return (   ...省略了..  );};export default Form;

4. 类型化 useRef

useRef 有两个主要用途

  • 保留一个自定义的可变值(它的值变更不会触发更新)。
  • 放弃对一个DOM对象的援用

类型化可变值

它基本上与 useState 雷同。想让useRef保留一个自定义的值,你须要通知它这个类型。

function Timer() {+  const intervalRef = useRef<number | undefined>();  useEffect(() => {    const id = setInterval(() => {      // ...    });    intervalRef.current = id;    return () => {      clearInterval(intervalRef.current);    };  });  // ...}

类型化 DOM 节点

在DOM节点上应用useRef的一个经典用例是解决input元素的focus

mport { useRef, useEffect } from 'react';const AutoFocusInput = () => {+  const inputRef = useRef(null);  useEffect(() => {+    inputRef.current.focus();  }, []);+  return <input ref={inputRef} type="text" value="前端柒八九" />;};export default AutoFocusInput;

TypeScript内置的DOM元素类型。这些类型的构造总是雷同的:

如果name是你正在应用的HTML标签的名称,相应的类型将是HTMLNameElement

这里有几个特例

  • <a>标签的类型为HTMLAnchorElement
  • <h1>标签的类型为HTMLHeadingElement

对于<input>,该类型的名称将是HTMLInputElement

mport { useRef, useEffect } from 'react';const AutoFocusInput = () => {+  const inputRef = useRef<HTMLInputElement>(null);  useEffect(() => {+    inputRef.current?.focus();  }, []);  return <input ref={inputRef} type="text" value="前端柒八九" />;};export default AutoFocusInput;

留神:在inputRef.current?.focus()上加了一个?。这是因为对于 TypeScriptinputRef.current可能是空的。在这种状况下,咱们晓得它不会是空的,因为它是在 useEffect 第一次运行之前由 React 填充的。


5. 类型化 forwardRef

有时想把ref转发给子组件。要做到这一点,在 React 中咱们必须用 forwardRef包装组件

import { ChangeEvent } from 'react';type Props = {  value: string,  handleChange: (event: ChangeEvent<HTMLInputElement>) => void,};const TextInput = ({ value, handleChange }: Props) => {  return <input type="text" value={value} onChange={handleChange} />;};

例如,存在一个组件TextInput而咱们想在父组件的调用处,通过ref来管制子组件input

此时,就须要用forwardRef来解决。

import { forwardRef, ChangeEvent } from 'react';type Props = {  value: string;  handleChange: (event: ChangeEvent<HTMLInputElement>) => void;};+const TextInput = forwardRef<HTMLInputElement, Props>(+  ({ value, handleChange }, ref) => {    return (+      <input ref={ref} type="text" value={value} onChange={handleChange} />    );  });

此语法只须要向 forwardRef 提供它应该期待的HTMLElement(在这种状况下是HTMLInputElement)。

有一点,须要指出:组件参数refprops的程序与泛型的<HTMLInputElement, Props>不一样。

6. 类型化 useEffect 和 useLayoutEffect

你不用给他们任何类型

惟一须要留神的是隐式返回useEffect外面的回调应该是什么都不返回,或者是一个会清理任何副作用的Destructor函数(析构函数,这个词借用了C++中类的说法)


7. 类型化 useMemo 和 useCallback

你不用给他们任何类型

8. 类型化 useContext

context提供类型是非常容易的。首先,为context创立一个类型,而后把它作为一个泛型提供给createContext函数。

import React, { createContext, useEffect, useState, ReactNode } from 'react';+type User = {+  name: string;+  email: string;+  freeTrial: boolean;+};+type AuthValue = {+  user: User | null;+  signOut: () => void;+};+const AuthContext = createContext<AuthValue | undefined>(undefined);type Props = {  children: ReactNode;};const AuthContextProvider = ({ children }: Props) => {  const [user, setUser] = useState(null);  const signOut = () => {    setUser(null);  };  useEffect(() => {    // 副作用解决  }, []);  return (+    <AuthContext.Provider value={{ user, signOut }}>      {children}+    </AuthContext.Provider>  );};export default AuthContextProvider;

一旦你向createContext提供了泛型,残余的事,都由ts为你代劳。

上述实现的一个问题是,就TypeScript而言,context的值能够是未定义的。也就是在咱们应用context的值的时候,可能取不到。此时,ts可能会拦截代码的编译。

如何解决context的值可能是未定义的状况呢。咱们针对context的获取能够应用一个自定义的hook

export const useAuthContext = () => {  const context = useContext(AuthContext);  if (context === undefined) {    throw new Error('useAuthContext必须在AuthContext上下文中应用');  }  return context;};

通过类型爱护,使得咱们在应用context的时候,总是有值的。


9. 类型化自定义hook

类型化自定义hook基本上和类型化一般函数一样

针对如何类型化一般函数,在一些教程中很多,一搜一大把。这里也不过多形容。

咱们来看一个比拟有意思的例子。有一个自定义hook,它想要返回一个元祖。

const useCustomHook = () => {  return ['abc', 123];};

TypeScipt 将扩充 useCustomHook 的返回类型为(number | string)[](一个能够蕴含数字或字符串的数组)。显然,这不是你想要的,你想要的是第一个参数总是一个字符串,第二个例子总是一个数字。

所以,这种状况下,咱们能够利用泛型对返回类型做一个限度解决。

const useCustomHook = (): [string, number] => {  return ['abc', 123];};

后记

分享是一种态度

参考资料:

  • React_Ts_类型化hook
  • 重写TS
  • TS官网

全文完,既然看到这里了,如果感觉不错,顺手点个赞和“在看”吧。