React + TS

如果有任何错误,请务必指出。
参考链接:https://github.com/typescript…

(一)构建基础的组件文件

  1. 首先我们需要导入 reactimport * as React from 'react'
  2. 为每个文件创建一个类组件。在一个文件内请尽量只写一个类组件,其他的请使用函数组件

    // ReactTs.tsx
    class TsExample extends React.Component<any, any> {}

    使用 extends 来继承 React.component 接口

  3. 我们注意到 React.Component 后面带了 <any, any> 这个奇怪的字符串。这是 React.Component 接口的类型参数。第一个是 props 的类型,第二个是 state 的类型。
  4. 为了给 propsstate 定义类型,我们可以使用 interface 关键字。我们可以将类型定义写在当前的组件文件,但是由于在当前文件中具有了 import 关键字,当前文件会成为一个模块,即无法被全局引用。所以建议新建一个文件 index.d.ts 来储存 interface\type

    // index.d.ts
    interface ITsExampleProps {
      name: string
    }

    注:exportimport 会将一个文件变成模块,即里面的变量不会被暴露到全局

  5. 接下来我们为这个组件初始化 state

    // ReactTs.tsx
    class TsExample extends React.Component<ITsExampleProps, ITsExampleState> {
      public state = {
        count: 0
      }
    }

    此处建议不要 public state = {}constructor 两种方法混用

  6. 为组件添加 UI:每个类组件都应该有一个 render 方法来进行 UI 渲染。render 方法中必须返回一个 ReactNodeReactNode 可以是一个HTMLElement 或者一个字符串,数字

    // ReactTs.tsx
    class TsExample extends React.Component<ITsExampleProps, ITsExampleState> {
      public state = {
        count: 0
      }
    
      public render() {
        let { count } = this.state
        return <div styleName="wrap">这里是 ui</div>
      }
    }
  7. 为组件添加并调用方法,使用 state

    // ReactTs.tsx
    class TsExample extends React.Component<IExampleProps, IExampleState> {
      public state = {
        count: 0,
        currentCount: 0
      }
    
      public plusCount = () => {
        this.setState({
          count: ++this.state.count
        })
      }
    
      public render() {
        let { count } = this.state
        return (
          <div styleName="">
            {/* 调用state */}
            <div>{count}</div>
            {/* 调用方法  */}
            <Button onClick={this.plusCount}>增加</Button>
            <div>
              <Button
                onClick={() => {
                  this.showCurrentCount(this.state.count)
                }}
              >
                当前数量
              </Button>
            </div>
          </div>
        )
      }
    }

8) 接下来我们将该组件作为一个子组件放到另一个组件中

// example.tsx
class Example extends React.Component<IExampleProps, IExampleState> {
  public render() {
    return (
      <div styleName="example">
        <ReactTs />
      </div>
    )
  }
}
  1. 为子组件传入一个参数

    // example.tsx
    class Example extends React.Component<IExampleProps, IExampleState> {
        public render() {
            return (
                <div styleName='example'>
                    <ReactTs name="React-Ts" />
                </div>
            )
        }
    }

10) 在子组件中使用参数:this.props.name

// ReactTs.tsx
public render() {
        let {count} = this.state
        return (
            <div styleName='React-Ts'>
                <div>名字:{this.props.name}</div>
                <div>增加数量:{count}</div>
                <div>当前数量:{this.state.currentCount}</div>
                <div><Button onClick={this.plusCount}>增加</Button></div>
                <Button
                                        onClick={() => { this.showCurrentCount(this.state.count) }}>
                      当前数量
                                </Button>
            </div>
        )
    }

(二)使用函数式组件

  1. 一般,在一个文件内,我们应该只写一个类组件,其他的子组件应该是函数式组件。
  2. 函数式组件类型可以使用React.FC,使用这个类型有个好处就是,提醒你必须返回一个ReactNode

    const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => {
      return <div></div>
    }

3) 在函数式组件中,我们无法像在类组件中一样使用state和生命钩子函数,但React提供了HOOK

4) 使用 useState 来存储函数组件的状态。在下面的例子中,我们使用了React.useState,传入color的初始值,返回了一个对象,我们使用解构,获得了color, setColor。其中 color 相当于 this.state.colorsetColor('green')相当于 this.setState({color: 'green'})

const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => {
  const [color, setColor] = React.useState('blue')

  const changeColor = () => {
    setColor('green')
  }

  return (
    <div>
      <div style={{ color }}>函数式组件</div>
      <Button type="primary" className="example-button" onClick={changeColor}>
        点击换色{' '}
      </Button>
    </div>
  )
}
  1. 假如你想像在类组件中使用 componentWillUnmountcomponentDidMountcomponentDidUpdate,你可以使用useEffectuseEffect接受一个回调函数,这个回调函数内代码会在 componentDidMountcomponentDidUpdate时执行。回调函数的返回值应该是一个函数,这个函数会在 componentWillUnmount被执行。

    const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => {
        const [color, setColor] = React.useState('blue')
      const [fontSize, setFontsize] = React.useState('16px')
    
      const changeColor = () => {
        setColor('green')
      }
    
      React.useEffect(() => {
        let timer = setInterval(() => {
          setFontsize('100px')
        }, 10000)
    
        return () => {
          clearInterval(timer)
        }
    
      return (
          <div>
          <div style={{color,fontSize}}>函数式组件</div>
          <Button type='primary' className='example-button' onClick={changeColor}>点击换色        </Button>
        </div>
      )
    }
  2. 假如我们需要操作 DOM,我们可以通过 useRef 来获取。text.current 就是被我们绑定的元素。注意:不允许直接操作 DOM,除非迫不得已

    const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => {
        const [color, setColor] = React.useState('blue')
      const [fontSize, setFontsize] = React.useState('16px')
    
      const text = React.useRef<HTMLDivElement>(null)
    
      const changeColor = () => {
        setColor('green')
      }
    
      const changeBgC = () => {
        (text.current as HTMLDivElement).style.backgroundColor = '#e9e9e9'
      }
    
      React.useEffect(() => {
        let timer = setInterval(() => {
          setFontsize('100px')
        }, 10000)
    
        return () => {
          clearInterval(timer)
        }
    
      return (
          <div>
          <div style={{color,fontSize}}>函数式组件</div>
          <Button type='primary' className='example-button' onClick={changeColor}>点击换色        </Button>
          <Button type='primary' className='example-button' onClick={changeBgC}>点击换背景色</Button>
        </div>
      )
    }

(三) 其他

  1. 有时候我们需要使用到默认参数 defaultProps ,这个默认参数是从多个组件的 props 中提取出来的。这时可以使用 交叉类型 & ,注意,请不要在交叉类型中使用相同的属性,就算使用了,也请不要为两个同名属性定义不同的基础类型,这样将会造成这个属性需要同时满足两种基础类型。
type propsType = typeof defaultProps & {
  count: number
}

const defaultProps = {
  name: 'world'
}

const DefaultPrppsExample = (props: propsType) => {
    return (
      <div>
        {props.name}
        {props.count}
      </div>
    )
  }

  // 在另一个组件中使用
;<DefaultPrppsExample count={1} name={'默认参数示例'} />
  1. createRefforwardRef

    在函数式组件中,我们可以使用 useRef 来获取到 React 元素。在类组件中,我们可以使用 createRef 来获取 React 元素。当我们需要获取某个组件中的元素时,使用 forwardRef 来获取这个组件内部的元素。还是不建议直接操纵 DOM 元素

    const RefExample = React.forwardRef((props: React.CSSProperties, ref: React.Ref<HTMLInputElement>) => {
        return <input style={{ ...props}} ref={ref} />
    })
    
    class TsExample extends React.Component<ITsExampleProps, ITsExampleState> {
      ...
      public state = {
        inputRef: React.createRef()
      }
    
        public getFocus = () => {
            if (this.state.inputRef) {
                // 注意:这里断言的类型应该是使用 RefObject,因为其里面的具有 current: T | null,
                // 如果使用的是 ReactRef ,这个类型是一个联合类型,里面有可能是 null
                ((this.state.inputRef as React.RefObject<HTMLInputElement>).current as HTMLInputElement).focus()
            }
    
        }
    
         ...
       <div styleName="example-item">
              <h3>使用 createRef 和 forwardRef</h3>
                    <RefExample color='red' ref={this.state.inputRef} />
                    <Button onClick={this.getFocus}>获取焦点</Button>
       </div>
      ...
    }

3) React 默认会把我们的子组件挂载到父组件上。当我们想自己指定挂载的元素时,就需要用到 createPortal 了。

这里写了一个简陋的蒙层组件(样式未完全实现),即使我们是TsExample中使用了这个组件,但这个组件还是会被挂载到一个插入 body 的元素上

注意:这里我们在父组件上绑定了一个点击事件,PortalExample 被点击时依然会冒泡进而触发父组件的点击事件

const PortalExample = (props: IPortarExampleProps) => {
    const [modelDOM, setModelDOM] = React.useState(document.createElement('div'))
    modelDOM.classList.add('modal')
    if (props.visible) {
        document.body.append(modelDOM)
        return ReactDOM.createPortal(
            <div styleName='modal-inner'>
                蒙层
            </div>,
            modelDOM)
    }else {
        if (modelDOM.parentNode) {
            modelDOM.parentNode.removeChild(modelDOM)
        }
        return null
    }

}

class TsExample extends React.Component<ITsExampleProps, ITsExampleState> {
  ...
  <div styleName="example-item" onClick={this.handleModalClick}>
             <h1>使用 createPortal 将子组件挂载到父组件以外的元素内</h1>
             <PortalExample visible={this.state.modalVisible} />
             <Button onClick={this.toggleModal}>切换modal</Button>
    </div>
  ...
}

(四) TS 类型基本使用

我们在使用类型注解的时候往往会使用以下类型

  1. 基本类型包括了js的七种基本类型 string, number, boolean, undefined, null, symbol,还有 void, never
  2. 可以使用 type, interface 来声明变量的形状

1. 联合类型

使用 | 来创造一个联合类型。例如:type pet = 'cat' | 'dog'

当我们需要规定一个函数参数的类型时,又恰巧有一个已经有一个接口已经满足了,我们可以使用 keyof 来快速生成一个联合类型

2. 类型守护

当我们在使用联合类型时,假如两个子类型 A 和 B 都是接口或类等时,我们预想的是要么是 A,要么是 B。但实际上,A&B 也是符合 A|B 的。

这个时候我们需要先用 in 关键字来判断是符合哪个类型再进行操作

interface IMan {
  handsNum: number
}

interface IDog {
  tailNum: number
}

public typeGuarding = (obj: IMan | IDog) => {
    if ('handsNum' in obj) {
      ...
    } else if ('tailNum' in obj) {
      ...
    }
  }

3. 交叉类型

使用 & 来创造一个联合类型,之前已经有示例了

4. 可选类型

可选类型其实不是一个类型,但在接口和函数参数中都有可选这个概念。

interface IPet {
    name: string
  age?: number
}

interface IMan {
  ...
}

const feedPet (pet: IPet, man?: IMan) => {
  ...
}

注意:函数的可选参数必须写在最后面

5. 默认参数

在函数声明中,直接为参数赋予一个值,可以起到默认参数的作用

interface IPetDefault {
    name: string
  age = 1
}

const feedPet (pet: IPet, man={name: '我'}) => {
  ...
}

6.enum

当我们需要使用枚举类型时,就需要用到enum 关键字。默认,第一个的值为 0,然后递增,可以手动指定每一个的值,之后的值也会递增。我们可以通过值查key,也可以通过key 查值

enum animal {
    乌龟,
    鳄鱼,
    麻雀,
    河马
}

animal['乌龟'] // 0
animal[0] // 乌龟

7. 类型断言

当我们给一个函数传入参数时,我们知道这个参数时符合被规定的参数类型的,但编译器是不知道的。为了让编译器能知道类型符合,我们就需要使用类型断言,关键字:as

type animalName = '乌龟' | '鳄鱼' | '麻雀' | '河马'

animal[this.state.animalName as animalName] // 0

类型断言里还有另一个类型:在值的后面使用 ! ,表示这个值不为 null

type animalNameWithNull = '乌龟' | '鳄鱼' | '麻雀' | '河马' | null

animal[(this.state.animalName as animalName)!] // 0

8. 使用类型扩展(type branding)

注:类型扩展是直译的结果,如果你知道这种用法的名字,请修正它

TS 在检查一个类型是否符合是,并不是查找类型的名字,而是比较类型的结构。也就是说,当你为两种类型userIdorderId 都定义了一样的结构:{name: string}。这时候,TS 会判定它们是同一种类型。请看下面的例子:

interface IOrderId {
  name: string
}

interface IUserId {
  name: string
}

let userId: IUserId = {
  name: 'userId'
}
let orderId: IOrderId = userId // ok

这里你是否会疑惑呢?两种不同的类型却可以相互赋值。这就是 结构类型检查 了。假如名字不同,类型就不同的就是 名称类型检查

那我们如何来避免呢?第一种方法是多加一个字段

interface MyUserId {
  name: string
  type: 'user'
}

interface MyOrderId {
  name: string
  type: 'order'
}

let myUserId: MyUserId = {
  name: 'user',
  type: 'user'
}

let myOrderId: MyOrderId = myUserId // error

还有另一种方法,那就是利用 unique symbol 和 交叉类型

type UserIdString = string & { readonly brand: unique symbol }
type OrderIdString = string & { readonly brand: unique symbol }

const getUniqueUserId = (id: string) => {
  return id as UserIdString
}

const getUniqueOrderId = (id: string) => {
  return id as OrderIdString
}

let uniqueUserId: UserIdString = getUniqueUserId('1')
let uniqueOrderId: OrderIdString = uniqueUserId // error

unique symbolsymbol 的子类型。我们在上面用交叉类型将 {readonly brand: unique symbol} 也加入到类型中去,这样,两种类型就算结构一样,它们也是不相同的

9. 函数重载

函数重载用于我们的函数拥有不同的输入或输出时

第一个函数声明应该是最精确的,因为编译器是从前往后开始匹配的。函数实现应该是兼容各个函数声明的。

这里函数重载的意义在于,如果们直接使用第三种声明,那么传入 3 个参数也是可以接受的,但是据我们所知, padding 是不接受 3 个参数的

// 重载
function padding(all: number)
function padding(topAndBottom: number, leftAndRight: number)
function padding(top: number, right: number, bottom: number, left: number)
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a
  } else if (c === undefined && d === undefined) {
    c = a
    d = b
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  }
}

10. 使用 typeof 关键字快速定义一个类型

有时候,我们在前面定义了一个变量,并使用类型推导(也就是说,这时候我们并没有明确地给它声明类型)。然后我们想要使用这个变量的类型了,怎么办?回去定义一下类型吗?使用 typeof 变量名 就可以获取到这个变量的类型了

11. 工具泛型的使用

TS 中内置了一些语法糖给我们使用,例如 Partial、Omit、Exclude

这里举例 partial 的用法:Patial 关键字可以让将一个接口内部的属性变成可选

interface dog {
  name: string
  bark: boolean
}

type littleDog = Partial<dog>

let dog1: littleDog = {
  name: 'dog1'
}

let dog2: littleDog = {
  bark: false
}

12. 模块声明

当我们需要引入某些模块时,这些模块又没有被声明过,这个时候就会报错了。我们只需要使用

decleare module 模块名 就可解决

decleare module "*.png"
import * as logo from 'logo.png'
    原文作者:NikolaChen
    原文地址: https://segmentfault.com/a/1190000019962867
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞