Typescript 与 React 结合使用的初学者指南
过去几个月我一直在使用 Typescript 开发 React 应用程序和库,想分享一些我一路走来学到的东西。这些是我在 Typescript 和 React 中大约 80% 时间使用的模式。
学习 Typescript 来开发 React 应用值得吗?绝对值得。我发现,强大的类型功能可以带来更可靠的代码和更快的迭代速度,尤其是在代码库规模较大的情况下。一开始你可能会感到沮丧,但随着你逐渐熟悉,你会发现,这些额外的样板代码非常值得。
如果你遇到困难,请记住你随时可以输入any
。Any 是你的朋友!
让我们来看一些例子。
使用 TypeScript 的基本 React 组件
那么,使用 TypeScript 编写的标准 React 组件是什么样的呢?让我们将它与标准的 JavaScript React 组件进行比较。
import React from 'react'
import PropTypes from 'prop-types'
export function StandardComponent({ children, title = 'Dr.' }) {
return (
<div>
{title}: {children}
</div>
)
}
StandardComponent.propTypes = {
title: PropTypes.string,
children: PropTypes.node.isRequired,
}
现在是打字稿版本:
import React, { ReactNode } from 'react'
export type StandardComponentProps = {
title?: string;
children: ReactNode;
}
export function StandardComponent({
children,
title = 'Dr.',
}: StandardComponentProps) {
return (
<div>
{title}: {children}
</div>
)
}
很像吧?我们用 TypeScript 类型替换了 propTypes。title 属性仍然是可选的,而 children 属性是必需的。我们导出了该类型,以便其他组件需要引用它。
扩展标准 HTML 属性
如果我们希望父组件能够提供额外的类型div
属性,例如aria-hidden
、style
,className
我们可以在组件中定义这些属性type
,也可以扩展内置类型。在下面的例子中,我们表示我们的组件div
除了title
和 之外,还接受任何标准 props children
。
import * as React from 'react'
export type SpreadingExampleProps = {
title?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function SpreadingExample({
children,
title = 'Dr.',
...other
}: SpreadingExampleProps) {
return (
<div {...other}>
{title}: {children}
</div>
)
}
处理事件
我们可以指定事件处理程序的类型,以确保事件参数的类型正确。以下示例演示了实现此目的的多种方法:
export type EventHandlerProps = {
onClick: (e: React.MouseEvent) => void;
}
export function EventHandler({ onClick }: EventHandlerProps) {
// handle focus events in a separate function
function onFocus(e: React.FocusEvent) {
console.log('Focused!', e.currentTarget)
}
return (
<button
onClick={onClick}
onFocus={onFocus}
onKeyDown={e => {
// When using an inline function, the appropriate argument signature
// is provided for us
}}
>
Click me!
</button>
)
}
不确定要使用哪个参数签名?在编辑器中,尝试将光标悬停在相关的事件处理程序属性上。
使用字符串文字
您是否有一个 prop,需要一个与一组预定义选项匹配的字符串?您可以使用 TypeScript 的字符串字面量来实现。
type Props = {
title: "senior" | "junior";
}
function Label({ title }: Props) {
return <div>Title: {title}</div>
}
现在,如果标题不是senior
或junior
,typescript 就会对你大喊大叫。
在 React 组件中使用泛型
这是一个更高级的功能,但功能非常强大。通常,你会在 React 组件中定义数据类型及其特定属性。假设你的组件需要一个配置文件对象。
type ProfileType = {
name: string;
image: string;
age: number | null;
}
type ProfilesProps = {
profiles: Array<ProfileType>;
}
function Profiles(props: ProfilesProps) {
// render a set of profiles
}
但现在让我们假设你有一个组件,它可以接受任何类型的数组。泛型类似于邮寄包裹。快递员(我们的组件)不需要知道你发送的包裹的具体内容,但发件人(父组件)希望收件人收到他们发送的内容。
以下是我们的操作方法:
type GenericsExampleProps<T> = {
children: (item: T) => React.ReactNode;
items: Array<T>;
}
export function GenericsExample<T>({
items,
children,
}: GenericsExampleProps<T>) {
return (
<div>
{items.map(item => {
return children(item)
})}
</div>
)
}
有点奇怪的例子……但它说明了要点。该组件接受任意类型的项目数组,遍历该数组并使用项目对象调用 children 作为渲染函数。当我们的父组件作为子组件提供渲染回调时,其item
类型将被正确显示!
没明白?没关系。我也不完全理解泛型,不过你不太可能经常用到它。而且你用 TypeScript 越多,就越能理解它的意义。
打字钩子
Hooks 大部分情况下都是开箱即用的。以下两个例外是:有时useRef
和useReducer
。下面的示例演示了如何键入 refs。
import * as React from 'react'
type HooksExampleProps = {}
export function HooksExample(props: HooksExampleProps) {
const [count, setCount] = React.useState(0)
const ref = React.useRef<HTMLDivElement | null>(null)
// start our timer
React.useEffect(
() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearTimeout(timer)
},
[count]
)
// measure our element
React.useEffect(
() => {
if (ref.current) {
console.log(ref.current.getBoundingClientRect())
}
},
[ref]
)
return <div ref={ref}>Count: {count}</div>
}
我们的状态是自动输入的,但我们手动输入了 our ,ref
以表明它要么为空,要么包含一个div
元素。当我们在函数中访问我们的 ref 时useEffect
,我们需要确保它不为空。
输入 reducer
减速器稍微复杂一点,但是如果类型正确的话确实很好。
// Yeah, I don't understand this either. But it gives us nice typing
// for our reducer actions.
type Action<K, V = void> = V extends void ? { type: K } : { type: K } & V
// our search response type
type Response = {
id: number;
title: string;
}
// reducer actions. These are what you'll "dispatch"
export type ActionType =
| Action<'QUERY', { value: string }>
| Action<'SEARCH', { value: Array<Response> }>
// the form that our reducer state takes
type StateType = {
searchResponse: Array<Response>;
query: string;
}
// our default state
const initialState: StateType = {
searchResponse: [];
query: '';
}
// the actual reducer
function reducer(state: StateType, action: ActionType) {
switch (action.type) {
case 'QUERY':
return {
...state,
query: action.value,
}
case 'SEARCH':
return {
...state,
searchResponse: action.value,
}
}
}
type ReducerExampleProps = {
query: string;
}
export function ReducerExample({ query }: ReducerExampleProps) {
const [state, dispatch] = React.useReducer(reducer, initialState)
React.useEffect(
() => {
if (query) {
// emulate async query
setTimeout(() => {
dispatch({
type: 'SEARCH',
value: [{ id: 1, title: 'Hello world' }],
})
}, 1000)
}
},
[query]
)
return state.searchResponse.map(response => (
<div key={response.id}>{response.title}</div>
))
}
使用typeof
和keyof
键入组件变量
假设我们想要构建一个具有多种外观的按钮,每个按钮都在一个具有一组键和样式的对象中定义,如下所示:
const styles = {
primary: {
color: 'blue',
},
danger: {
color: 'red',
},
}
我们的按钮组件应该接受一个type
prop,该 prop 可以是对象的任意键styles
(例如,“primary”或“danger”)。我们可以很容易地输入以下内容:
const styles = {
primary: {
color: 'blue',
},
danger: {
color: 'red',
},
}
// creates a reusable type from the styles object
type StylesType = typeof styles
// ButtonType = any key in styles
export type ButtonType = keyof StylesType
type ButtonProps = {
type: ButtonType
}
export function Button({ type = 'primary' }: ButtonProps) {
return <button style={styles[type]}>My styled button</button>
}
这些示例应该能帮你完成 80% 的学习。如果你遇到困难,可以参考一些现有的开源示例。
Sancho UI是一组使用 typescript 和 emotion 构建的 react 组件。
Blueprint是另一套用 typescript 构建的 React 组件。
文章来源:https://dev.to/bmcmahen/a-beginners-guide-to-using-typescript-with-react-7m6