大家好,我是「柒八九」。
在前几天,我们开辟了--「TypeScript实战系列」,主要讲TS
在React
中的应用实战。
大家如果对React
了解/熟悉的话,想必都听过Hook
。在当下的React
开发中,函数组件大行其道。而Hook
就是为了给「函数组件添加内部状态还有处理副作用」的。换句话说,Hook
已经在现在的React
的开发中, 变得不可替代。
而,今天我们就简单的聊聊,如何利用TS
对Hook
进行类型化处理。有一点需要特别指出,对hook
进行类型化处理,需要利用「泛型」的语法,如果对泛型没有一个大体的了解,还是需要异步一些常规资料中,先进行简单的学习。
- TS_React:使用泛型来改善类型
- typescriptlang_generics
好了,天不早了。我们开始「粗发」。
你能所学到的知识点
❝
React
各种hook
的类型化处理,总有一款,让你欲罢不能 ❞
文章概要
- 依赖类型推断
- 类型化 useState
- 类型化 useReducer
- 类型化 useRef
- 类型化 forwardRef
- 类型化 useEffect 和 useLayoutEffect
- 类型化 useMemo 和 useCallback
- 类型化 useContext
- 类型化自定义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>('前端柒八九');
类型推断错误
有时,推断的类型是错误的(或者「限制性太强」不是你想要的类型)。这种情况经常发生在React
的useState
「默认值」中。比方说,name
的初始值是null
。
const [name, setName] = useState(null);
在这种情况下,TypeScript
会推断出name
是null类型
的(这意味着它「总是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
有两样东西要类型化处理:state
和action
。
这里有一个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 操作符
代码语言:javascript复制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}`);
}
};
使用类型别名
代码语言:javascript复制 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.type
是username
,它就会自动知道它应该是第一种情况,并且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()
上加了一个?
。这是因为对于 TypeScript
,inputRef.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
)。
❝「有一点,需要指出」:组件参数
ref
和props
的顺序与泛型的<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)[]
(一个可以包含数字或字符串的「数组」)。显然,这不是你想要的,你想要的是第一个参数总是一个字符串,第二个例子总是一个数字。
所以,这种情况下,我们可以利用「泛型」对返回类型做一个限制处理。
代码语言:javascript复制const useCustomHook = (): [string, number] => {
return ['abc', 123];
};
后记
「分享是一种态度」。
参考资料:
- React_Ts_类型化hook
- 重写TS
- TS官