React + TS 实践

React + TS 实践

原文连接

引入 React

  1. 推荐使用
import * as React from 'react'

import * as ReactDOM from 'react-dom'
  1. 需要添加额外的配置"allowSyntheticDefaultImports": true
import React from 'react'

import ReactDOM from 'react-dom'

函数式组件声明方式

  1. 推荐 React.FunctionComponent === React.FC(我们项目中也是这么用的)
// Great
type AppProps = {
  message: string
}

const App: React.FC<AppProps> = ({ message, children }) => (
  <div>
    {message}
    {children}
  </div>
)

使用用 React.FC 声明函数组件和普通声明以及 PropsWithChildren 的区别是:

  1. React.FC 显式地定义了返回类型,其他方式是隐式推导的

  2. React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全

  3. 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

如果出现类型不兼容问题,建议使用以下两种方式:

  1. PropsWithChildren

这种方式可以为你省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode

type AppProps = React.PropsWithChildren<{ message: string }>
const App = ({ message, children }: AppProps) => (
  <div>
    {message}
    {children}
  </div>
)
  1. 直接声明:
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)

这两种的区别在于:

  1. 第一种方式的 ref1.current 是只读的(read-only),并且可以传递给内置的 ref 属性,绑定 DOM 元素 ;

  2. 第二种方式的 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?

有几种常用规则:

  1. 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口

  2. 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强

interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:

  1. 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 事件,有两个定义参数类型的方法。

  1. 第一种方法使用推断的方法签名(例如: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>
  )
}
  1. 第二种方法强制使用 @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>
  )
}

什么时候使用泛型

  1. 需要作用到很多类型的时候

  2. 需要被用到很多地方的时候,比如常用的工具泛型 Partial

如果需要深 Partial 我们可以通过泛型递归来实现

type DeepPartial<T> = T extends Function
  ? T
  : T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T
type PartialedWindow = DeepPartial<Window>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容