最近总结了一些 React + TS 的使用经验,欢迎大家指正~
TS官方文档
目录
- 简单的例子
- 组件属性常用类型
- 组件默认属性
- React 实例类型
- React Hooks 中使用 TS
- 常用的 TS 操作符
- 其他补充
简单的例子
React.FC 泛型中接收组件 props 的类型
// 也可以用 type TestCompProps = {}
interface TestCompProps {
username: string
status?: 'wait' | 'pass' | 'reject' // 可选属性
onChange: (index: number) => void
}
const TestComp: React.FC<TestCompProps> = (props) => {
const { username, status, onChange } = props
return (
<div>
<p>用户名:{username}</p>
{status && (
<p>状态:{status}</p>
)}
<button onClick={() => onChange(66)}>点我</button>
</div>
)
}
组件属性常用类型
基本
interface PropsType {
title: string
count: number
disabled: boolean
other?: string // 可选属性
}
数组
interface PropsType {
infoList: string[] // 等同 Array<string>
// 数组项是对象
objArr: {
adId: string
mediaUrl: number
}[]
excelRow: [number, string, string] // 一般用于确定长度的数组
status: 'success' | 'fail' | 'wait' // |表示或
twoDimArr: any[][] // 任意类型的二维数组
}
对象
interface PropsType {
obj1: object // 任何对象 但不可访问属性(不建议用)
obj2: {} // 和object一样
// 普通
obj3: {
id: string | number
title: string
}
// 不确定键名
looseObj1: {
[key: string]: { id: number; title: string }
}
// 同上,Record会对键值对进行映射
looseObj2: Record<string, { id: number; title: string }>
}
函数
interface PropsType {
// 函数类型 但不需要调用的(不推荐使用)
onSomething: Function
// 最简单的,void表示没有返回值
onDoSometing: () => void
// 带参数的函数
onChange: (index: number) => void
// dom事件函数(这里是鼠标事件 HTMLButtonElement表示<button />元素)
onClick(event: React.MouseEvent<HTMLButtonElement>): void
}
DOM 相关的TS类型 基本都在 lib.dom.ts 里
组件默认属性
可以合并组件默认属性的类型
interface CompType {
id: number
title: string
}
const defaultProps = { age: 18 }
const Comp: React.FC<CompType & typeof defaultProps> = ({ id, title, age }) => {
return <div>ID:{id},标题:{title},年龄:{age}</div>
}
Comp.defaultProps = defaultProps
const TestPage: React.FC = () => {
return (
<div>
<Comp title="20211202世界完全对称日" id={886} />
</div>
)
}
对于上面的合并默认属性类型的情况 又想复用这个组件的类型,直接使用 props: React.ComponentProps<typeof Comp> 会报错!需要自己创建一个泛型来解决:
下面的 ComponentProps 意思是 当 T 继承自 React.ComponentType<infer P> 或 React.Component<infer P> 时,返回 JSX.LibraryManagedAttributes<T, P> 否则为 never
T 为 CompType & typeof defaultProps,P 是当前组件里传了什么属性
type ComponentProps<T> = T extends
| React.ComponentType<infer P>
| React.Component<infer P>
? JSX.LibraryManagedAttributes<T, P>
: never
const GreetComp = (props: ComponentProps<typeof Comp>) => {
const { id, title, age } = props
return <div>ID:{id},标题:{title},年龄:{age}</div>
}
infer 一般与 extends 配合使用,这里使用 infer 对未知类型 P 进行类型推断
TS 内置类型 ReturnType 的原理实现就使用了 infer
// 用于提取函数类型的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
React 实例类型
interface AppProps2 {
children1: JSX.Element // html节点
children2: JSX.Element | JSX.Element[]
children3: React.ReactChildren // 只是别名 没有真实作用(不建议使用)
children4: React.ReactChild[] // 比ReactChildren好一些(不建议使用)
// 接收所有的React类型 包括null(比如JSX.Element、React.ReactChild都属于它)
children: React.ReactNode
functionChildren: (name: string) => React.ReactNode
style?: React.CSSProperties // css属性
// 表单事件函数
onChange?: React.FormEventHandler<HTMLInputElement>
}
JSX.Element 必须是纯粹的html节点 不接收字符串,比如 div,一般使用 React.ReactNode 是最佳选择,有个技巧 比如 HTMLElement 当不知道具体使用哪种类型时,在 vscode 中按住 ctrl 点击某个类型可以查看 TS 的类型定义源文件
React Hooks 中使用 TS
useState
可以自动推断类型,也可以接收一个泛型
type IUserInfo = {
id: number
username: string
}
const App = () => {
const [ logged, setLogged] = useState(false) // 类型自动推断
const [ userInfo, setUserInfo ] = useState<IUserInfo | null>(null)
// ...
}
useEffect
useEffect(() => {
let timer = setTimeout(() => {
console.log('do somthing!')
}, timerMs)
// 组件卸载时
return () => {
clearTimeout(timer)
}
}, [props.times])
useRef
一般用于保存任何非作用于视图更新的可变值,和 DOM 引用,视图更新用 useState
const TextInputWithFocusButton: React.FC = () => {
const inputEl = useRef<HTMLInputElement>(null)
const onButtonClick = () => {
// .current 指向下面已挂载到 DOM 上的 <input/> 元素
inputEl.current?.focus()
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus!</button>
</>
)
}
有个小技巧,如果觉得每次写 inputEl.current?. 比较麻烦,可以使用 ! 告诉 TS inputEl 一定是有值的
// ...
const inputEl = useRef<HTMLInputElement>(null!)
const onButtonClick = () => {
inputEl.current.focus() // OK
}
// ...
另外对于 setTimeout 和 setInterval 的类型可以使用 NodeJS.Timeout
// ...
const timer = useRef<NodeJS.Timeout | null>(null)
const onButtonClick = () => {
timer.current = setTimeout(() => {}, 1000)
}
// ...
useReducer
当 state 逻辑比较复杂时,使用 useReducer 更合适
import { useReducer } from 'react'
type ActionType =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: string }
// 状态初始值
const initialState = { count: 1 }
// 如果明确知道值的类型,直接用typeof进行推断 比较方便
function reducer(state: typeof initialState, action: ActionType) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload }
case 'decrement':
return { count: state.count - action.payload }
default:
throw new Error()
}
}
const Counter: React.FC = () => {
const [ state, dispatch ] = useReducer(reducer, initialState)
return (
<div>
<button onClick={() => { dispatch({ type: 'decrement', payload: 1 }) }}>减</button>
<div>{state.count}</div>
<button onClick={() => { dispatch({ type: 'increment', payload: 1 }) }}>加</button>
</div>
)
}
useImperativeHandle 句柄
待更新……
自定义hooks
点击目标元素之外区域的自定义hook,参数 ref 类型为 React.RefObject
function useOutsideClick(ref: React.RefObject<HTMLElement>, fn: () => void) {
const handleClickOutside = (e: MouseEvent) => {
// 如果被点击的元素不包含目标元素,就调用 fn 回调函数
if (ref.current && !ref.current.contains(e.target as HTMLElement)) fn()
}
useEffect(() => {
document.addEventListener('click', handleClickOutside)
// 组件卸载时移除事件监听
return () => {
document.removeEventListener('click', handleClickOutside)
}
}, [ref])
}
使用上面hook
const Comp: React.FC = () => {
const btnEl = useRef<HTMLButtonElement | null>(null!)
useOutsideClick(compRef, () => {
// do somthing...
})
return <button ref={btnEl}>目标元素</button>
}
常用的 TS 操作符
?: 可选参数
type IUser = {
id: number
avatar?: string // 可选的
}
?. 可选链操作符
在类型中 ?: 表示可选参数,?. 是可选链操作符(TypeScript 3.7+)当访问对象属性时 如果属性不存在就返回 undefind 存在就直接返回属性值(如果属性有值为 null 就返回 null 为 undefined 就返回 undefined,不会混淆影响)
// 可选链操作符
const obj = { age: 18 }
const val = obj?.title // undefined
let result = obj.fn?.() // obj上有fn函数就调用 没有则不调用
上面编译成 ES5:
var val = a === null || a === void 0 ? void 0 : a.b;
?? 空值合并操作符
用于变量或属性赋值:?? 左边数据为 null 或 undefined 就赋值右边的,设置默认值。
用于函数:?? 左边的函数返回不为 false 就执行右边函数。
// 1.设置默认值
const theFoo = obj.foo ?? '123'
// 2.函数操作
function leftFuncOk1() {
return null // undefined也一样
}
function leftFuncOk2() {
return true
}
function leftFuncNo() {
return false
}
function print() {
console.log('有结果~')
}
// 这两都会执行print!
leftFuncOk1() ?? print()
leftFuncOk2() ?? print()
// 则不会执行print
leftFuncNo() ?? print()
另一种是 ??=,同理当变量为 null 或 undefined 时,就就赋值右边的
// 这里foo被设为66
let foo = null
foo ??= 66
! 非空断言操作符
! 本身可置反布尔值,但如果放在一个变量后面叫 非空断言操作符,即告诉 TS 该属性肯定是有值的,不会是 null 或 undefined(避免由此引起的一些类型错误)
// 非空断言操作符
// #1
let x!: number
// #2
const boxWidth = document.querySelector('#box')!.offsetWidth
// #3
const el = useRef<HTMLDivElement | null>(null!)
| & 运算符
| 是联合类型,用在基本类型时 可以理解为“或”
const todo = (id: string | number) => {
console.log(id)
}
// OK
todo(123)
todo('123')
type StatusType = 'success' | 'fail'
let resStatus: StatusType = 'success'
resStatus = 'wait' // 报错
如果是 type(类型别名)或 interface(接口)的联合类型,新声明的对象可以包含几个类型的所有属性,但只能访问它们共有的属性,比如下面只能访问 obj.name
interface Foo {
name: string
age: number
}
interface Bar {
name: string
onChange: () => void
}
// OK
let obj: Foo | Bar = {
name: 'Li',
age: 18,
onChange: () => { console.log(123) }
}
console.log(obj.name) // OK
console.log(obj.age) // 报错
obj.onChange() // 报错
& 是交叉类型,修改下上面的 obj 为交叉类型,obj 的属性必须同时满足 Foo 和 Bar,一般用于合并两个类型
// 报错!
let obj: Foo | Bar = {
name: 'Li',
age: 18
}
// OK
let obj: Foo | Bar = {
name: 'HaHa',
age: 20,
onChange: () => { console.log(123) }
}
其他补充
不建议随便使用 any,有办法时尽量不用,如果不知道是什么类型 可以用泛型解决
======== 将不定期更新 ========