我最近在Reddit的LearnTypeScript子论坛上偶然发现了一个关于自定义React钩子的问题。一个用户想创建一个切换的自定义钩子,并坚持常规React钩子的命名惯例:返回一个数组,你在调用钩子时对其进行解构。例如:useState 。
typescript const [state, setState] = useState(0)
为什么是一个数组?因为你这个数组的字段没有名字,而且你可以自己设置名字。
const [count, setCount] = useState(0)
const [darkMode, setDarkMode] = useState(true)
所以很自然地,如果你有一个类似的模式,你也想返回一个数组。
一个自定义的切换钩子可能看起来像这样。
export const useToggle = (initialValue: boolean) => {
const [value, setValue] = useState(initialValue)
const toggleValue = () => setValue(!value)
return [value, toggleValue]
}
没有什么不正常的。我们唯一要设置的类型是我们输入参数的类型。让我们试着使用它。
export const Body = () => {
const [isVisible, toggleVisible] = useToggle(false)
return (
<>
{/* It very much booms here! 💥 */ }
<button onClick={toggleVisible}>Hello</button>
{isVisible && <div>World</div>}
</>
)
}
那么,为什么会失败呢?TypeScript的错误信息对此有很详细的说明。类型'boolean | (() => void)'不能赋值给类型'((event: MouseEvent<HTMLButtonElement, MouseEvent>) => void)| 未定义'。类型'false'不能分配给类型'((event: MouseEvent<HTMLButtonElement, MouseEvent>) => void)| 未定义'。
这可能是非常隐蔽的。但是我们应该注意的是第一个类型,它被声明为不兼容:boolean | (() => void)' 。这来自于返回一个数组。数组是一个任意长度的列表,它可以容纳尽可能多的元素。从useToggle 中的返回值,TypeScript推断出一个数组类型。由于value 的类型是布尔型(很好!),而toggleValue 的类型是(() => void) (一个什么都不返回的函数),TypeScript 告诉我们在这个数组中两种类型都有可能。
而这就是破坏与onClick 的兼容性的原因。onClick 期望是一个函数。很好,toggleValue (或toggleVisible )是一个函数。但是根据TypeScript,它也可以是一个布尔值!轰!"。TypeScript告诉你要明确,或者至少要做类型检查。
但是我们不应该需要做额外的类型检查。我们的代码是非常清晰的。错的是类型。因为我们不是在处理一个数组。
让我们用一个不同的名字:Tuple。虽然数组是一个可以有任何长度的值的列表,但我们确切地知道在一个元组中得到多少个值。通常情况下,我们也知道元组中每个元素的类型。
所以我们不应该返回一个数组,而应该返回一个元组,useToggle 。问题是:在JavaScript中,数组和元组是无法区分的。在TypeScript的类型系统中,我们可以区分它们。
选项1:添加一个返回元组的类型#
第一种可能性。让我们对我们的返回类型有意为之。由于TypeScript--正确地!- 推断出一个数组,我们必须告诉TypeScript,我们期待一个元组。
// add a return type here
export const useToggle =
(initialValue: boolean): [boolean, () => void] => {
const [value, setValue] = useState(initialValue)
const toggleValue = () => setValue(!value)
return [value, toggleValue]
}
使用[boolean, () => void] 作为返回类型,TypeScript会检查我们在这个函数中是否返回了一个元组。TypeScript不再进行推断,而是确保你的预期返回类型与实际值相匹配。瞧,你的代码不再抛出错误了。
选项2:作为const#
对于一个元组,我们知道我们期待的元素有多少,并且知道这些元素的类型。这听起来像是一个用const断言来冻结类型的工作。
export const useToggle = (initialValue: boolean) => {
const [value, setValue] = useState(initialValue)
const toggleValue = () => setValue(!value)
// here, we freeze the array to a tuple
return [value, toggleValue] as const
}
现在的返回类型是readonly [boolean, () => void] ,因为as const ,确保你的值是恒定的,而不是可改变的。这种类型在语义上有一点不同,但在现实中,你不会在useToggle 外改变你的返回值。因此,作为readonly 会稍微正确一些。