如何优雅地在 React 中使用TypeScript,看这一篇就够了!(3)

简介: 毕业已有3月有余,工作用的技术栈主要是React hooks + TypeScript。其实在单独使用 TypeScript 时没有太多的坑,不过和React结合之后就会复杂很多。本文就来聊一聊TypeScript与React一起使用时经常遇到的一些类型定义的问题。阅读本文前,希望你能有一定的React和TypeScript基础

四、事件处理


1. Event 事件类型


在开发中我们会经常在事件处理函数中使用event事件对象,比如在input框输入时实时获取输入的值;使用鼠标事件时,通过 clientX、clientY 获取当前指针的坐标等等。

我们知道,Event是一个对象,并且有很多属性,这时很多人就会把 event 类型定义为any,这样的话TypeScript就失去了它的意义,并不会对event事件进行静态检查,如果一个键盘事件触发了下面的方法,也不会报错:


const handleEvent = (e: any) => {
    console.log(e.clientX, e.clientY)
}
复制代码


由于Event事件对象中有很多的属性,所以我们也不方便把所有属性及其类型定义在一个interface中,所以React在声明文件中给我们提供了Event事件对象的类型声明。

常见的Event 事件对象如下:

  • 剪切板事件对象:ClipboardEvent<T = Element>
  • 拖拽事件对象:DragEvent<T = Element>
  • 焦点事件对象:FocusEvent<T = Element>
  • 表单事件对象:FormEvent<T = Element>
  • Change事件对象:ChangeEvent<T = Element>
  • 键盘事件对象:KeyboardEvent<T = Element>
  • 鼠标事件对象:MouseEvent<T = Element, E = NativeMouseEvent>
  • 触摸事件对象:TouchEvent<T = Element>
  • 滚轮事件对象:WheelEvent<T = Element>
  • 动画事件对象:AnimationEvent<T = Element>
  • 过渡事件对象:TransitionEvent<T = Element>


可以看到,这些Event事件对象的泛型中都会接收一个Element元素的类型,这个类型就是我们绑定这个事件的标签元素的类型,标签元素类型将在下面的第五部分介绍。

来看一个简单的例子:


type State = {
  text: string;
};
const App: React.FC = () => {  
  const [text, setText] = useState<string>("")
  const onChange = (e: React.FormEvent<HTMLInputElement>): void => {
    setText(e.currentTarget.value);
  };
  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
    </div>
  );
}
复制代码


这里就给onChange方法的事件对象定义为了FormEvent类型,并且作用的对象时一个HTMLInputElement类型的标签(input标签)

可以来看下MouseEvent事件对象和ChangeEvent事件对象的类型声明,其他事件对象的声明形似也类似:


interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
  altKey: boolean;
  button: number;
  buttons: number;
  clientX: number;
  clientY: number;
  ctrlKey: boolean;
  /**
    * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
    */
  getModifierState(key: string): boolean;
  metaKey: boolean;
  movementX: number;
  movementY: number;
  pageX: number;
  pageY: number;
  relatedTarget: EventTarget | null;
  screenX: number;
  screenY: number;
  shiftKey: boolean;
}
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
  target: EventTarget & T;
}
复制代码


在很多事件对象的声明文件中都可以看到 EventTarget 的身影。这是因为,DOM的事件操作(监听和触发),都定义在EventTarget接口上。EventTarget 的类型声明如下:

interface EventTarget {
    addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
    dispatchEvent(evt: Event): boolean;
    removeEventListener(type: string, listener?: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}
复制代码


比如在change事件中,会使用的e.target来获取当前的值,它的的类型就是EventTarget。来看下面的例子:

<input
  onChange={e => onSourceChange(e)}
  placeholder="最多30个字"
/>
const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.value.length > 30) {
      message.error('请长度不能超过30个字,请重新输入');
      return;
    }
    setSourceInput(e.target.value);
};
复制代码


这里定义了一个input输入框,当触发onChange事件时,会调用onSourceChange方法,该方法的参数e的类型就是:React.ChangeEvent,而e.target的类型就是EventTarget:

网络异常,图片无法展示
|


在来看一个例子:

questionList.map(item => (
    <div
      key={item.id}
    role="button"
    onClick={e => handleChangeCurrent(item, e)}
    >
    // 组件内容...
    </div>
)
const handleChangeCurrent = (item: IData, e: React.MouseEvent<HTMLDivElement>) => {
    e.stopPropagation();
    setCurrent(item);
};
复制代码


这点代码中,点击某个盒子,就将它设置为当前的盒子,方便执行其他操作。当鼠标点击盒子时,会触发handleChangeCurren方法,该方法有两个参数,第二个参数是event对象,在方法中执行了e.stopPropagation();是为了阻止冒泡事件,这里的stopPropagation()实际上并不是鼠标事件MouseEvent的属性,它是合成事件上的属性,来看看声明文件中的定义:


interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
  //...     
}
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
  //...
}
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
interface BaseSyntheticEvent<E = object, C = any, T = any> {
  nativeEvent: E;
  currentTarget: C;
  target: T;
  bubbles: boolean;
  cancelable: boolean;
  defaultPrevented: boolean;
  eventPhase: number;
  isTrusted: boolean;
  preventDefault(): void;
  isDefaultPrevented(): boolean;
  stopPropagation(): void;
  isPropagationStopped(): boolean;
  persist(): void;
  timeStamp: number;
  type: string;
}
复制代码


可以看到,这里的stopPropagation()是一层层的继承来的,最终来自于BaseSyntheticEvent合成事件类型。原生的事件集合SyntheticEvent就是继承自合成时间类型。SyntheticEvent<T = Element, E = Event>泛型接口接收当前的元素类型和事件类型,如果不介意这两个参数的类型,完全可以这样写:


<input 
  onChange={(e: SyntheticEvent<Element, Event>)=>{
    //... 
  }}
/>
复制代码


2. 事件处理函数类型


说完事件对象类型,再来看看事件处理函数的类型。React也为我们提供了贴心的提供了事件处理函数的类型声明,来看看所有的事件处理函数的类型声明:

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];
type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
// 剪切板事件处理函数
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
// 复合事件处理函数
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
// 拖拽事件处理函数
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
// 焦点事件处理函数
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
// 表单事件处理函数
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
// Change事件处理函数
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
// 键盘事件处理函数
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
// 鼠标事件处理函数
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
// 触屏事件处理函数
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
// 指针事件处理函数
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
// 界面事件处理函数
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
// 滚轮事件处理函数
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
// 动画事件处理函数
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
// 过渡事件处理函数
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;
复制代码


这里面的T的类型也都是Element,指的是触发该事件的HTML标签元素的类型,下面第五部分会介绍。

EventHandler会接收一个E,它表示事件处理函数中 Event 对象的类型。bivarianceHack 是事件处理函数的类型定义,函数接收一个 Event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void。


还看上面的那个例子:

type State = {
  text: string;
};
const App: React.FC = () => {  
  const [text, setText] = useState<string>("")
  const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setText(e.currentTarget.value);
  };
  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
    </div>
  );
}
复制代码


这里给onChange方法定义了方法的类型,它是一个ChangeEventHandler的类型,并且作用的对象时一个HTMLImnputElement类型的标签(input标签)。


五、HTML标签类型


1. 常见标签类型


在项目的依赖文件中可以找到HTML标签相关的类型声明文件:

网络异常,图片无法展示
|


所有的HTML标签的类型都被定义在 intrinsicElements 接口中,常见的标签及其类型如下:

a: HTMLAnchorElement;
body: HTMLBodyElement;
br: HTMLBRElement;
button: HTMLButtonElement;
div: HTMLDivElement;
h1: HTMLHeadingElement;
h2: HTMLHeadingElement;
h3: HTMLHeadingElement;
html: HTMLHtmlElement;
img: HTMLImageElement;
input: HTMLInputElement;
ul: HTMLUListElement;
li: HTMLLIElement;
link: HTMLLinkElement;
p: HTMLParagraphElement;
span: HTMLSpanElement;
style: HTMLStyleElement;
table: HTMLTableElement;
tbody: HTMLTableSectionElement;
video: HTMLVideoElement;
audio: HTMLAudioElement;
meta: HTMLMetaElement;
form: HTMLFormElement; 
复制代码


那什么时候会使用到标签类型呢,上面第四部分的Event事件类型和事件处理函数类型中都使用到了标签的类型。上面的很多的类型都需要传入一个ELement类型的泛型参数,这个泛型参数就是对应的标签类型值,可以根据标签来选择对应的标签类型。这些类型都继承自HTMLElement类型,如果使用时对类型类型要求不高,可以直接写HTMLELement。比如下面的例子:


<Button
  type="text"
  onClick={(e: React.MouseEvent<HTMLElement>) => {
  handleOperate();
  e.stopPropagation();
}}
  >
    <img
  src={cancelChangeIcon}
  alt=""
    />
    取消修改
</Button>
复制代码


其实,在直接操作DOM时也会用到标签类型,虽然我们现在通常会使用框架来开发,但是有时候也避免不了直接操作DOM。比如我在工作中,项目中的某一部分组件是通过npm来引入的其他组的组件,而在很多时候,我有需要动态的去个性化这个组件的样式,最直接的办法就是通过原生JavaScript获取到DOM元素,来进行样式的修改,这时候就会用到标签类型。


来看下面的例子:

document.querySelectorAll('.paper').forEach(item => {
  const firstPageHasAddEle = (item.firstChild as HTMLDivElement).classList.contains('add-ele');
  if (firstPageHasAddEle) {
    item.removeChild(item.firstChild as ChildNode);
  }
})
复制代码


这是我最近写的一段代码(略微删改),在第一页有个add-ele元素的时候就删除它。这里我们将item.firstChild断言成了HTMLDivElement类型,如果不断言,item.firstChild的类型就是ChildNode,而ChildNode类型中是不存在classList属性的,所以就就会报错,当我们把他断言成HTMLDivElement类型时,就不会报错了。很多时候,标签类型可以和断言(as)一起使用。


后面在removeChild时又使用了as断言,为什么呢?item.firstChild不是已经自动识别为ChildNode类型了吗?因为TS会认为,我们可能不能获取到类名为paper的元素,所以item.firstChild的类型就被推断为ChildNode | null,我们有时候比TS更懂我们定义的元素,知道页面一定存在paper 元素,所以可以直接将item.firstChild断言成ChildNode类型。


2. 标签属性类型


众所周知,每个HTML标签都有自己的属性,比如Input框就有value、width、placeholder、max-length等属性,下面是Input框的属性类型定义:

interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
  accept?: string | undefined;
  alt?: string | undefined;
  autoComplete?: string | undefined;
  autoFocus?: boolean | undefined;
  capture?: boolean | string | undefined;
  checked?: boolean | undefined;
  crossOrigin?: string | undefined;
  disabled?: boolean | undefined;
  enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined;
  form?: string | undefined;
  formAction?: string | undefined;
  formEncType?: string | undefined;
  formMethod?: string | undefined;
  formNoValidate?: boolean | undefined;
  formTarget?: string | undefined;
  height?: number | string | undefined;
  list?: string | undefined;
  max?: number | string | undefined;
  maxLength?: number | undefined;
  min?: number | string | undefined;
  minLength?: number | undefined;
  multiple?: boolean | undefined;
  name?: string | undefined;
  pattern?: string | undefined;
  placeholder?: string | undefined;
  readOnly?: boolean | undefined;
  required?: boolean | undefined;
  size?: number | undefined;
  src?: string | undefined;
  step?: number | string | undefined;
  type?: string | undefined;
  value?: string | ReadonlyArray<string> | number | undefined;
  width?: number | string | undefined;
  onChange?: ChangeEventHandler<T> | undefined;
}
复制代码


如果我们需要直接操作DOM,就可能会用到元素属性类型,常见的元素属性类型如下:

  • HTML属性类型:HTMLAttributes
  • 按钮属性类型:ButtonHTMLAttributes
  • 表单属性类型:FormHTMLAttributes
  • 图片属性类型:ImgHTMLAttributes
  • 输入框属性类型:InputHTMLAttributes
  • 链接属性类型:LinkHTMLAttributes
  • meta属性类型:MetaHTMLAttributes
  • 选择框属性类型:SelectHTMLAttributes
  • 表格属性类型:TableHTMLAttributes
  • 输入区属性类型:TextareaHTMLAttributes
  • 视频属性类型:VideoHTMLAttributes
  • SVG属性类型:SVGAttributes
  • WebView属性类型:WebViewHTMLAttributes


一般情况下,我们是很少需要在项目中显式的去定义标签属性的类型。如果子级去封装组件库的话,这些属性就能发挥它们的作用了。来看例子(来源于网络,仅供学习):


import React from 'react';
import classNames from 'classnames'
export enum ButtonSize {
    Large = 'lg',
    Small = 'sm'
}
export enum ButtonType {
    Primary = 'primary',
    Default = 'default',
    Danger = 'danger',
    Link = 'link'
}
interface BaseButtonProps {
    className?: string;
    disabled?: boolean;
    size?: ButtonSize;
    btnType?: ButtonType;
    children: React.ReactNode;
    href?: string;    
}
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> // 使用 交叉类型(&) 获得我们自己定义的属性和原生 button 的属性
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLAnchorElement> // 使用 交叉类型(&) 获得我们自己定义的属性和原生 a标签 的属性
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps> //使用 Partial<> 使两种属性可选
const Button: React.FC<ButtonProps> = (props) => {
    const { 
        disabled,
        className, 
        size,
        btnType,
        children,
        href,
        ...restProps  
    } = props;
    const classes = classNames('btn', className, {
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]: size,
        'disabled': (btnType === ButtonType.Link) && disabled  // 只有 a 标签才有 disabled 类名,button没有
    })
    if(btnType === ButtonType.Link && href) {
        return (
            <a 
              className={classes}
              href={href}
              {...restProps}
            >
                {children}
            </a>
        )
    } else {
        return (
            <button 
              className={classes}
              disabled={disabled} // button元素默认有disabled属性,所以即便没给他设置样式也会和普通button有一定区别
              {...restProps}
            >
                {children}
            </button>
        )
    }
}
Button.defaultProps = {
    disabled: false,
    btnType: ButtonType.Default
}
export default Button;
复制代码


这段代码就是用来封装一个buttom按钮,在button的基础上添加了一些自定义属性,比如上面将button的类型使用交叉类型(&)获得自定义属性和原生 button 属性 :

type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> 
复制代码


可以看到,标签属性类型在封装组件库时还是很有用的,更多用途可以自己探索~

相关文章
|
2月前
|
前端开发 JavaScript 测试技术
从零开始搭建react+typescript+antd+redux+less+vw自适应项目
从零开始搭建react+typescript+antd+redux+less+vw自适应项目
60 0
|
2月前
|
前端开发 定位技术 API
react+typescript接入百度地图
react+typescript接入百度地图
53 0
|
2月前
|
JavaScript
react+typescript通过window.xxx挂载属性报错的解决方案
react+typescript通过window.xxx挂载属性报错的解决方案
43 0
|
11天前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript与React的完美结合
【4月更文挑战第30天】React和TypeScript在前端开发中备受推崇。React以其组件化、高性能和灵活的生态系统引领UI构建,而TypeScript通过静态类型检查和面向对象特性增强了代码的健壮性和可维护性。两者结合,能提升开发效率,降低错误,使React组件结构更清晰。通过安装TypeScript,配置tsconfig.json,然后用TypeScript编写和打包代码,可实现两者的无缝集成。这种结合为前端开发带来更强的代码质量和团队协作效果,随着技术发展,其应用将更加广泛。
|
11天前
|
前端开发 JavaScript 安全
【亮剑】探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式
【4月更文挑战第30天】本文探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式。文章分为三部分:首先解释了样式传递的必要性,包括模块化、主题化和动态样式以及TypeScript集成。接着介绍了内联样式的基本用法和最佳实践,展示了一个使用内联样式自定义按钮颜色的例子。最后,讨论了使用CSS模块和TypeScript接口处理复杂样式的方案,强调了它们在组织和重用样式方面的优势。结合TypeScript,确保了样式的正确性和可维护性,为开发者提供了灵活的样式管理策略。
|
2月前
|
前端开发 JavaScript 安全
使用React、TypeScript和Ant Design构建现代化前端应用
使用React、TypeScript和Ant Design构建现代化前端应用
31 0
|
2月前
react+typescript给state和props定义指定类型
react+typescript给state和props定义指定类型
17 1
|
2月前
react+typescript装饰器写法报错的解决办法
react+typescript装饰器写法报错的解决办法
23 1
|
18天前
|
JavaScript 编译器
TypeScript中类型守卫:缩小类型范围的艺术
【4月更文挑战第23天】TypeScript中的类型守卫是缩小类型范围的关键技术,它帮助我们在运行时确保值的精确类型,提升代码健壮性和可读性。类型守卫包括`typeof`(检查原始类型)、`instanceof`(检查类实例)和自定义类型守卫。通过这些方法,我们可以更好地处理联合类型、泛型和不同数据源,降低运行时错误,提高代码质量。
|
11天前
|
JavaScript 安全 前端开发
【TypeScript技术专栏】TypeScript中的类型推断与类型守卫
【4月更文挑战第30天】TypeScript的类型推断与类型守卫是提升代码安全的关键。类型推断自动识别变量类型,减少错误,包括基础、上下文、最佳通用和控制流类型推断。类型守卫则通过`typeof`、`instanceof`及自定义函数在运行时确认变量类型,确保类型安全。两者结合使用,优化开发体验,助力构建健壮应用。