React + TS 实践
引入 React
- 推荐使用
import * as React from 'react'
import * as ReactDOM from 'react-dom'
- 需要添加额外的配置
"allowSyntheticDefaultImports": true
import React from 'react'
import ReactDOM from 'react-dom'
函数式组件声明方式
- 推荐 React.FunctionComponent === React.FC(我们项目中也是这么用的)
// Great
type AppProps = {
message: string
}
const App: React.FC<AppProps> = ({ message, children }) => (
<div>
{message}
{children}
</div>
)
使用用 React.FC 声明函数组件和普通声明以及 PropsWithChildren 的区别是:
React.FC 显式地定义了返回类型,其他方式是隐式推导的
React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全
React.FC 为 children 提供了隐式的类型(ReactElement | null)
比如以下用法 React.FC 会报类型错误:
const App: React.FC = props => props.children
const App: React.FC = () => [1, 2, 3]
const App: React.FC = () => 'hello'
解决方法:
const App: React.FC<{}> = props => props.children as any
const App: React.FC<{}> = () => [1, 2, 3] as any
const App: React.FC<{}> = () => 'hello' as any
// 或者
const App: React.FC<{}> = props => (props.children as unknown) as JSX.Element
const App: React.FC<{}> = () => ([1, 2, 3] as unknown) as JSX.Element
const App: React.FC<{}> = () => ('hello' as unknown) as JSX.Element
如果出现类型不兼容问题,建议使用以下两种方式:
- PropsWithChildren
这种方式可以为你省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode
type AppProps = React.PropsWithChildren<{ message: string }>
const App = ({ message, children }: AppProps) => (
<div>
{message}
{children}
</div>
)
- 直接声明:
type AppProps = {
message: string
children?: React.ReactNode
}
const App = ({ message, children }: AppProps) => (
<div>
{message}
{children}
</div>
)
Hook
useState<T>
默认情况下会为你根据你设置的初始值自动进行推导,但是如果初始值为 null 需要显示的声明类型
type User = {
name: string
age: number
}
const [user, setUser] = React.useState<User | null>(null)
useRef<T>
当初始值为 null 时,有两种创建方式:
const ref1 = React.useRef<HTMLInputElement>(null)
const ref2 = React.useRef<HTMLInputElement | null>(null)
这两种的区别在于:
第一种方式的 ref1.current 是只读的(read-only),并且可以传递给内置的 ref 属性,绑定 DOM 元素 ;
第二种方式的 ref2.current 是可变的(类似于声明类的成员变量)
这两种方式在使用时,都需要对类型进行检查:
const onButtonClick = () => {
ref1.current?.focus()
ref2.current?.focus()
}
在某种情况下,可以省去类型检查,通过添加 ! 断言,不推荐:
// Bad
function MyComponent() {
const ref1 = React.useRef<HTMLDivElement>(null!)
React.useEffect(() => {
// 不需要做类型检查,需要人为保证ref1.current.focus一定存在
doSomethingWith(ref1.current.focus())
})
return <div ref={ref1}> etc </div>
}
useEffect
返回值只能是函数或者是 undefined
useMemo<T> / useCallback<T>
useMemo 和 useCallback 都可以直接从它们返回的值中推断出它们的类型
useCallback 的参数必须制定类型,否则 ts 不会报错,默认指定为 any
const value = 10
// 自动推断返回值为 number
const result = React.useMemo(() => value * 2, [value])
// 自动推断 (value: number) => number
const multiply = React.useCallback((value: number) => value * multiplier, [
multiplier,
])
同时也支持传入泛型, useMemo 的泛型指定了返回值类型,useCallback 的泛型指定了参数类型
// 也可以显式的指定返回值类型,返回值不一致会报错
const result = React.useMemo<string>(() => 2, [])
// 类型“() => number”的参数不能赋给类型“() => string”的参数。
const handleChange = React.useCallback<
React.ChangeEventHandler<HTMLInputElement>
>(evt => {
console.log(evt.target.value)
}, [])
自定义 Hook
需要注意,自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 Union 类型,而我们实际需要的是数组里里每一项的具体类型,需要手动添加 const 断言 进行处理
function useLoading() {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
// 实际需要: [boolean, typeof load] 类型
// 而不是自动推导的:(boolean | typeof load)[]
return [isLoading, load] as const
}
如果使用 const 断言遇到问题,也可以直接定义返回类型:
export function useLoading(): [
boolean,
(aPromise: Promise<any>) => Promise<any>
] {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
return [isLoading, load]
}
如果有大量的自定义 Hook 需要处理,这里有一个方便的工具方法可以处理 tuple 返回值:
function tuplify<T extends any[]>(...elements: T) {
return elements
}
function useLoading() {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
// (boolean | typeof load)[]
return [isLoading, load]
}
function useTupleLoading() {
const [isLoading, setState] = React.useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.then(() => setState(false))
}
// [boolean, typeof load]
return tuplify(isLoading, load)
}
默认属性 defaultProps
在我们进行迁移的时候,我们可以通过自定义组件 props 类型的必选或者可选来规定参数是否必须传,而对于 angular 中有初始值的@Input 属性,我们可以使用 React 组件的默认值来为其初始化
但是大部分文章都不推荐使用 defaultProps 来进行初始化,推荐使用默认参数值来代替默认属性:
但是这种方式对于属性特别多的时候又很鸡肋
type GreetProps = { age?: number }
const Greet = ({ age = 21 }: GreetProps) => {
/* ... */
}
Types or Interfaces
implements 与 extends 静态操作,不允许存在一种或另一种实现的情况,所以不支持使用联合类型:
使用 Type 还是 Interface?
有几种常用规则:
在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口
在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强
interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:
- type 类型不能二次编辑,而 interface 可以随时扩展
interface Animal {
name: string
}
// 可以继续在原有属性基础上,添加新属性:color
interface Animal {
color: string
}
/********************************/
type Animal = {
name: string
}
// type类型不支持属性扩展
// Error: Duplicate identifier 'Animal'
type Animal = {
color: string
}
获取未导出的 Type
某些场景下我们在引入第三方的库时会发现想要使用的组件并没有导出我们需要的组件参数类型或者返回值类型,这时候我们可以通过 ComponentProps/ ReturnType 来获取到想要的类型。
// 获取参数类型
import { Button } from 'library' // 但是未导出props type
type ButtonProps = React.ComponentProps<typeof Button> // 获取props
type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick
const AlertButton: React.FC<AlertButtonProps> = props => (
<Button onClick={() => alert('hello')} {...props} />
)
// 获取返回值类型
function foo() {
return { baz: 1 }
}
type FooReturn = ReturnType<typeof foo> // { baz: number }
Props
通常我们使用 type 来定义 Props,为了提高可维护性和代码可读性,在日常的开发过程中我们希望可以添加清晰的注释。
现在有这样一个 type
type OtherProps = {
name: string
color: string
}
增加相对详细的注释,使用时会更清晰,需要注意,注释需要使用 /**/ , // 无法被 vscode 识别
// Great
/**
* @param color color
* @param children children
* @param onClick onClick
*/
type Props = {
/** color */
color?: string
/** children */
children: React.ReactNode
/** onClick */
onClick: () => void
}
// type Props
// @param color — color
// @param children — children
// @param onClick — onClick
const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
return (
<button style={{ backgroundColor: color }} onClick={onClick}>
{children}
</button>
)
}
常用的 React 属性类型
export declare interface AppBetterProps {
children: React.ReactNode // 一般情况下推荐使用,支持所有类型 Great
functionChildren: (name: string) => React.ReactNode
style?: React.CSSProperties // 传递style对象
onChange?: React.FormEventHandler<HTMLInputElement>
}
Forms and Events
onChange
change 事件,有两个定义参数类型的方法。
- 第一种方法使用推断的方法签名(例如:React.FormEvent <HTMLInputElement> :void)
import * as React from 'react'
type changeFn = (e: React.FormEvent<HTMLInputElement>) => void
const App: React.FC = () => {
const [state, setState] = React.useState('')
const onChange: changeFn = e => {
setState(e.currentTarget.value)
}
return (
<div>
<input type="text" value={state} onChange={onChange} />
</div>
)
}
- 第二种方法强制使用 @types / react 提供的委托类型,两种方法均可。
import * as React from 'react'
const App: React.FC = () => {
const [state, setState] = React.useState('')
const onChange: React.ChangeEventHandler<HTMLInputElement> = e => {
setState(e.currentTarget.value)
}
return (
<div>
<input type="text" value={state} onChange={onChange} />
</div>
)
}
onSubmit
如果不太关心事件的类型,可以直接使用 React.SyntheticEvent,如果目标表单有想要访问的自定义命名输入,可以使用类型扩展
import * as React from 'react'
const App: React.FC = () => {
const onSubmit = (e: React.SyntheticEvent) => {
e.preventDefault()
const target = e.target as typeof e.target & {
password: { value: string }
} // 类型扩展
const password = target.password.value
}
return (
<form onSubmit={onSubmit}>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>
)
}
不要在 type 或 interface 中使用函数声明
保持一致性,类型/接口的所有成员都通过相同的语法定义。
--strictFunctionTypes 在比较函数类型时强制执行更严格的类型检查
事件处理
我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们通过 clientX、clientY 去获取指针的坐标。
大家可能会想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。
幸运的是 React 的声明文件提供了 Event 对象的类型声明。
Event 事件对象类型
ClipboardEvent<T = Element> 剪切板事件对象
DragEvent<T =Element> 拖拽事件对象
ChangeEvent<T = Element> Change 事件对象
KeyboardEvent<T = Element> 键盘事件对象
MouseEvent<T = Element> 鼠标事件对象
TouchEvent<T = Element> 触摸事件对象
WheelEvent<T = Element> 滚轮时间对象
AnimationEvent<T = Element> 动画事件对象
TransitionEvent<T = Element> 过渡事件对象
事件处理函数类型
当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler 类型别名,通过不同事件的 EventHandler 的类型别名来定义事件处理函数的类型
type EventHandler<E extends React.SyntheticEvent<any>> = {
bivarianceHack(event: E): void
}['bivarianceHack']
type ReactEventHandler<T = Element> = EventHandler<React.SyntheticEvent<T>>
type ClipboardEventHandler<T = Element> = EventHandler<React.ClipboardEvent<T>>
type DragEventHandler<T = Element> = EventHandler<React.DragEvent<T>>
type FocusEventHandler<T = Element> = EventHandler<React.FocusEvent<T>>
type FormEventHandler<T = Element> = EventHandler<React.FormEvent<T>>
type ChangeEventHandler<T = Element> = EventHandler<React.ChangeEvent<T>>
type KeyboardEventHandler<T = Element> = EventHandler<React.KeyboardEvent<T>>
type MouseEventHandler<T = Element> = EventHandler<React.MouseEvent<T>>
type TouchEventHandler<T = Element> = EventHandler<React.TouchEvent<T>>
type PointerEventHandler<T = Element> = EventHandler<React.PointerEvent<T>>
type UIEventHandler<T = Element> = EventHandler<React.UIEvent<T>>
type WheelEventHandler<T = Element> = EventHandler<React.WheelEvent<T>>
type AnimationEventHandler<T = Element> = EventHandler<React.AnimationEvent<T>>
type TransitionEventHandler<T = Element> = EventHandler<
React.TransitionEvent<T>
>
Promise 类型
在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。Promise<T> 是一个泛型类型,T 泛型变量用于确定 then 方法时接收的第一个回调函数的参数类型。
泛型参数的组件
type Props<T> = {
name: T
name2?: T
}
const TestC: <T>(props: Props<T>) => React.ReactElement = ({ name, name2 }) => {
return (
<div className="test-b">
TestB--{name}
{name2}
</div>
)
}
const TestD = () => {
return (
<div>
<TestC<string> name="123" />
</div>
)
}
什么时候使用泛型
需要作用到很多类型的时候
需要被用到很多地方的时候,比如常用的工具泛型 Partial
如果需要深 Partial 我们可以通过泛型递归来实现
type DeepPartial<T> = T extends Function
? T
: T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T
type PartialedWindow = DeepPartial<Window>