React + TypeScript实践

avatar
@北京字节跳动网络技术有限公司

❗️准备知识 :

  • 熟悉 React

本文档参考 TypeScript 最新版本

如何引入React

import * as React from 'react'

import * as ReactDOM from 'react-dom'

这种引用方式被证明是最可靠的一种方式, 推荐使用。

而另外一种引用方式:

import React from 'react'

import ReactDOM from 'react-dom'

需要添加额外的配置:"allowSyntheticDefaultImports": true

函数式组件的声明方式

声明的几种方式

第一种:也是比较推荐的一种,使用 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),但是目前,提供的类型存在一些 issue(问题)

比如以下用法 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

在通常情况下,使用 React.FC 的方式声明最简单有效,推荐使用;如果出现类型不兼容问题,建议使用以下两种方式:

第二种:使用 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>

)

Hooks

useState<T>

大部分情况下,TS 会自动为你推导 state 的类型:



// `val`会推导为boolean类型, toggle接收boolean类型参数

const [val, toggle] = React.useState(false)



// obj会自动推导为类型: {name: string}

const [obj] = React.useState({ name: 'sj' })



// arr会自动推导为类型: string[]

const [arr] = React.useState(['One', 'Two'])

使用推导类型作为接口/类型:



export default function App() {

  // user会自动推导为类型: {name: string}

  const [user] = React.useState({ name: 'sj', age: 32 })

  const showUser = React.useCallback((obj: typeof user) => {

    return `My name is ${obj.name}, My age is ${obj.age}`

  }, [])



  return <div className="App">用户: {showUser(user)}</div>

}

但是,一些状态初始值为空时(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 ref = React.useRef(0)



React.useEffect(() => {

  ref.current += 1

}, [])

这两种方式在使用时,都需要对类型进行检查:



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

useEffect 需要注意回调函数的返回值只能是函数或者 undefined



function App() {

  // undefined作为回调函数的返回值

  React.useEffect(() => {

    // do something...

  }, [])



  // 返回值是一个函数

  React.useEffect(() => {

    // do something...

    return () => {}

  }, [])

}

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)

}, [])

自定义Hooks

需要注意,自定义 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

大部分文章都不推荐使用 defaultProps , 相关讨论可以**参考链接**

推荐方式:使用默认参数值来代替默认属性:



type GreetProps = { age?: number }

const Greet = ({ age = 21 }: GreetProps) => {

  /* ... */

}

defaultProps类型

TypeScript3.0+ 在默认属性 的类型推导上有了极大的改进,虽然尚且存在一些边界case仍然存在问题,不推荐使用,如果有需要使用的场景,可参照如下方式:



type IProps = {

  name: string

}

const defaultProps = {

  age: 25,

}



// 类型定义

type GreetProps = IProps & typeof defaultProps



const Greet = (props: GreetProps) => <div></div>

Greet.defaultProps = defaultProps

// 使用

const TestComponent = (props: React.ComponentProps<typeof Greet>) => {

  return <h1 />

}

const el = <TestComponent name="foo" />

Types or Interfaces

在日常的react开发中 interface 和 type 的使用场景十分类似

implements 与 extends 静态操作,不允许存在一种或另一种实现的情况,所以不支持使用联合类型:



class Point {

  x: number = 2

  y: number = 3

}



interface IShape {

  area(): number

}



type Perimeter = {

  perimeter(): number

}



type RectangleShape = (IShape | Perimeter) & Point



class Rectangle implements RectangleShape {

  // 类只能实现具有静态已知成员的对象类型或对象类型的交集。

  x = 2

  y = 3

  area() {

    return this.x + this.y

  }

}



interface ShapeOrPerimeter extends RectangleShape {}

// 接口只能扩展使用静态已知成员的对象类型或对象类型的交集

使用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

}

在使用的过程中,hover 对应类型会有如下展示



// type OtherProps = {

//   name: string;

//   color: string;

// }

const OtherHeading: React.FC<OtherProps> = ({ name, color }) => (

  <h1>My Website Heading</h1>

)

增加相对详细的注释,使用时会更清晰,需要注意,注释需要使用 /**/ , // 无法被 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>

  )

}

常用Props ts类型

基础属性类型



type AppProps = {

  message: string

  count: number

  disabled: boolean

  /** array of a type! */

  names: string[]

  /** string literals to specify exact string values, with a union type to join them together */

  status: 'waiting' | 'success'

  /** 任意需要使用其属性的对象(不推荐使用,但是作为占位很有用) */

  obj: object

  /** 作用和`object`几乎一样,和 `Object`完全一样 */

  obj2: {}

  /** 列出对象全部数量的属性 (推荐使用) */

  obj3: {

    id: string

    title: string

  }

  /** array of objects! (common) */

  objArr: {

    id: string

    title: string

  }[]

  /** 任意数量属性的字典,具有相同类型*/

  dict1: {

    [key: string]: MyTypeHere

  }

  /** 作用和dict1完全相同 */

  dict2: Record<string, MyTypeHere>

  /** 任意完全不会调用的函数 */

  onSomething: Function

  /** 没有参数&返回值的函数 */

  onClick: () => void

  /** 携带参数的函数 */

  onChange: (id: number) => void

  /** 携带点击事件的函数 */

  onClick(event: React.MouseEvent<HTMLButtonElement>): void

  /** 可选的属性 */

  optional?: OptionalType

}

常用React属性类型



export declare interface AppBetterProps {

  children: React.ReactNode // 一般情况下推荐使用,支持所有类型 Great

  functionChildren: (name: string) => React.ReactNode

  style?: React.CSSProperties // 传递style对象

  onChange?: React.FormEventHandler<HTMLInputElement>

}



export declare interface AppProps {

  children1: JSX.Element // 差, 不支持数组

  children2: JSX.Element | JSX.Element[] // 一般, 不支持字符串

  children3: React.ReactChildren // 忽略命名,不是一个合适的类型,工具类类型

  children4: React.ReactChild[] // 很好

  children: React.ReactNode // 最佳,支持所有类型 推荐使用

  functionChildren: (name: string) => React.ReactNode // recommended function as a child render prop type

  style?: React.CSSProperties // 传递style对象

  onChange?: React.FormEventHandler<HTMLInputElement> // 表单事件, 泛型参数是event.target的类型

}

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>

  )

}

Operators

常用的操作符,常用于类型判断

  • typeof and instanceof: 用于类型区分
  • keyof: 获取object的key

  • O[K]: 属性查找

  • [K in O]: 映射类型

  • + or - or readonly or ?: 加法、减法、只读和可选修饰符

  • x ? Y : Z: 用于泛型类型、类型别名、函数参数类型的条件类型

  • !: 可空类型的空断言

  • as: 类型断言

  • is: 函数返回类型的类型保护

Tips

使用查找类型访问组件属性类型

通过查找类型减少 type 的非必要导出,如果需要提供复杂的 type,应当提取到作为公共 API 导出的文件中。

现在我们有一个 Counter 组件,需要 name 这个必传参数:



// counter.tsx

import * as React from 'react'

export type Props = {

  name: string

}

const Counter: React.FC<Props> = props => {

  return <></>

}

export default Counter

在其他引用它的组件中我们有两种方式获取到 Counter 的参数类型

第一种是通过 typeof 操作符(推荐)



// Great

import Counter from './d-tips1'

type PropsNew = React.ComponentProps<typeof Counter> & {

  age: number

}



const App: React.FC<PropsNew> = props => {

  return <Counter {...props} />

}

export default App

第二种是通过在原组件进行导出



import Counter, { Props } from './d-tips1'



type PropsNew = Props & {

  age: number

}



const App: React.FC<PropsNew> = props => {

  return (

    <>

      <Counter {...props} />

    </>

  )

}

export default App

不要在type或interface中使用函数声明

保持一致性,类型/接口的所有成员都通过相同的语法定义。

--strictFunctionTypes 在比较函数类型时强制执行更严格的类型检查,但第一种声明方式下严格检查不生效。

✅

interface ICounter {

  start: (value: number) => string

}



❌

interface ICounter1 {

  start(value: number): string

}



🌰

interface Animal {}

interface Dog extends Animal {

  wow: () => void

}

interface Comparer<T> {

  compare: (a: T, b: T) => number

}

declare let animalComparer: Comparer<Animal>

declare let dogComparer: Comparer<Dog>

animalComparer = dogComparer // Error

dogComparer = animalComparer // Ok



interface Comparer1<T> {

  compare(a: T, b: T): number

}

declare let animalComparer1: Comparer1<Animal>

declare let dogComparer1: Comparer1<Dog>

animalComparer1 = dogComparer // Ok

dogComparer1 = animalComparer // Ok

事件处理

我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们通过 clientX、clientY 去获取指针的坐标。

大家可能会想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。



function handleEvent(event: any) {

  console.log(event.clientY)

}

试想下当我们注册一个 Touch 事件,然后错误的通过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里我们已经将 event 设置为 any 类型,导致 TypeScript 在编译时并不会提示我们错误, 当我们通过 event.clientY 访问时就有问题了,因为 Touch 事件的 event 对象并没有 clientY 这个属性。

通过 interface 对 event 对象进行类型声明编写的话又十分浪费时间,幸运的是 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>

>

bivarianceHack 为事件处理函数的类型定义,函数接收一个 event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void

关于为何是用bivarianceHack而不是(event: E): void,这与strictfunctionTypes选项下的功能兼容性有关。(event: E): void,如果该参数是派生类型,则不能将其传递给参数是基类的函数。



class Animal {

  private x: undefined

}

class Dog extends Animal {

  private d: undefined

}



type EventHandler<E extends Animal> = (event: E) => void

let z: EventHandler<Animal> = (o: Dog) => {} // fails under strictFunctionTyes



type BivariantEventHandler<E extends Animal> = {

  bivarianceHack(event: E): void

}['bivarianceHack']

let y: BivariantEventHandler<Animal> = (o: Dog) => {}

Promise 类型

在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。Promise<T> 是一个泛型类型,T 泛型变量用于确定 then 方法时接收的第一个回调函数的参数类型。



type IResponse<T> = {

  message: string

  result: T

  success: boolean

}



async function getResponse(): Promise<IResponse<number[]>> {

  return {

    message: '获取成功',

    result: [1, 2, 3],

    success: true,

  }

}



getResponse().then(response => {

  console.log(response.result)

})

首先声明 IResponse 的泛型接口用于定义 response 的类型,通过 T 泛型变量来确定 result 的类型。然后声明了一个 异步函数 getResponse 并且将函数返回值的类型定义为 Promise<IResponse<number[]>> 。最后调用 getResponse 方法会返回一个 promise 类型,通过 then 调用,此时 then 方法接收的第一个回调函数的参数 response 的类型为,{ message: string, result: number[], success: boolean} 。

泛型参数的组件

下面这个组件的name属性都是指定了传参格式,如果想不指定,而是想通过传入参数的类型去推导实际类型,这就要用到泛型。



const TestB = ({ name, name2 }: { name: string; name2?: string }) => {

  return (

    <div className="test-b">

      TestB--{name}

      {name2}

    </div>

  )

}

如果需要外部传入参数类型,只需 ->



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>

  )

}

什么时候使用泛型

当你的函数,接口或者类:

  • 需要作用到很多类型的时候,举个🌰

当我们需要一个 id 函数,函数的参数可以是任何值,返回值就是将参数原样返回,并且其只能接受一个参数,在 js 时代我们会很轻易地甩出一行

const id = arg => arg

由于其可以接受任意值,也就是说我们的函数的入参和返回值都应该可以是任意类型,如果不使用泛型,我们只能重复的进行定义



type idBoolean = (arg: boolean) => boolean

type idNumber = (arg: number) => number

type idString = (arg: string) => string

// ...

如果使用泛型,我们只需要



function id<T>(arg: T): T {

  return arg

}



// 或



const id1: <T>(arg: T) => T = arg => {

  return arg

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

功能是将类型的属性变成可选, 注意这是浅 Partial。

type Partial<T> = { [P in keyof T]?: T[P] }

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

type DeepPartial<T> = T extends Function

  ? T

  : T extends object

  ? { [P in keyof T]?: DeepPartial<T[P]> }

  : T



type PartialedWindow = DeepPartial<Window>

字节跳动懂车帝团队招聘

我们是字节跳动旗下懂车帝产品线,目前业务上正处于高速发展阶段,懂车帝自2017年8月正式诞生,仅三年时间已经是汽车互联网行业第二。

现在前端团队主流的技术栈是React、Typescript,主要负责懂车帝App、M站、PC站、懂车帝小程序产品矩阵、商业化海量业务,商业数据产品等。我们在类客户端、多宿主、技术建站、中后台系统、全栈、富交互等多种应用场景都有大量技术实践,致力于技术驱动业务发展,探索所有可能性。

加入懂车帝,一起打造汽车领域最专业最开放的前端团队!

简历直达:dcar_fe@bytedance.com

邮件标题:应聘+城市+岗位名称