TypeScript在React中的优雅写法

目录

前言

组件 Props

基础类型

对象类型

函数类型

React 相关类型

React元素相关

原生DOM相关

类组件

函数组件

与hooks的结合

useState

userReducer

useRef

自定义 hook

React合成事件相关

Event 事件对象类型

styles

扩展组件的 Props

redux相关

第三方库

规约

其他


前言

其实如果运用熟练的话,TS 只是在第一次开发的时候稍微多花一些时间去编写类型,后续维护、重构的时候就会发挥它神奇的作用了,还是非常推荐长期维护的项目使用它的。

组件 Props

先看几种定义 Props 经常用到的类型:

基础类型

type BasicProps = {
  message: string;
  count: number;
  disabled: boolean;
  /** 数组类型 */
  names: string[];
  /** 用「联合类型」限制为下面两种「字符串字面量」类型 */
  status: "waiting" | "success";
};

对象类型

type ObjectOrArrayProps = {
  /** 如果你不需要用到具体的属性 可以这样模糊规定是个对象 ❌ 不推荐 */
  obj: object;
  obj2: {}; // 同上
  /** 拥有具体属性的对象类型 ✅ 推荐 */
  obj3: {
    id: string;
    title: string;
  };
  /** 对象数组 😁 常用 */
  objArr: {
    id: string;
    title: string;
  }[];
  /** key 可以为任意 string,值限制为 MyTypeHere 类型 */
  dict1: {
    [key: string]: MyTypeHere;
  };
  dict2: Record<string, MyTypeHere>; // 基本上和 dict1 相同,用了 TS 内置的 Record 类型。
}
//通过接口定义相应的结构
interface Item {
	name: string,
	icon: string,
    url: string,
	status:boolean,
	initShow:boolean,
    copanyStatus:boolean
}
interface MyObject {
       [key: string]: any;
}

函数类型

// 基本语法
interface InterfaceName {
  (param1: parameterType1,param2:parameterType2... ): returnType;
}

// type定义
type FunctionProps = {
  /** 任意的函数类型 ❌ 不推荐 不能规定参数以及返回值类型 */
  onSomething: Function;
  /** 没有参数的函数 不需要返回值 😁 常用 */
  onClick: () => void;
  /** 带函数的参数 😁 非常常用 */
  onChange: (id: number) => void;
  /** 另一种函数语法 参数是 React 的按钮事件 😁 非常常用 */
  onClick(event: React.MouseEvent<HTMLButtonElement>): void;
  (name:string):string;
  /** 可选参数类型 😁 非常常用 */
  optional?: OptionalType;
}

React 相关类型

export declare interface AppProps {
  children1: JSX.Element; // ❌ 不推荐 没有考虑数组
  children2: JSX.Element | JSX.Element[]; // ❌ 不推荐 没有考虑字符串 children
  children4: React.ReactChild[]; // 稍微好点 但是没考虑 null
  children: React.ReactNode; // ✅ 包含所有 children 情况
  functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点的函数
  style?: React.CSSProperties; // ✅ 推荐 在内联 style 时使用
  // ✅ 推荐原生 button 标签自带的所有 props 类型
  // 也可以在泛型的位置传入组件 提取组件的 Props 类型
  props: React.ComponentProps<"button">;
  // ✅ 推荐 利用上一步的做法 再进一步的提取出原生的 onClick 函数类型 
  // 此时函数的第一个参数会自动推断为 React 的点击事件类型
  onClickButton:React.ComponentProps<"button">["onClick"]
}

React元素相关

React元素相关的类型主要包括ReactNode、ReactElement、JSX.Element。

ReactNode。表示任意类型的React节点,这是个联合类型,包含情况众多;

ReactElement/JSX。从使用表现上来看,可以认为这两者是一致的,属于ReactNode的子集,表示“原生的DOM组件”或“自定义组件的执行结果”。

使用示例如下:

const MyComp: React.FC<{ title: string; }> = ({title}) => <h2>{title}</h2>;

// ReactNode
const a: React.ReactNode =
  null ||
  undefined || <div>hello</div> || <MyComp title="world" /> ||
  "abc" ||
  123 ||
  true;

// ReactElement和JSX.Element
const b: React.ReactElement = <div>hello world</div> || <MyComp title="good" />;

const c: JSX.Element = <MyComp title="good" /> || <div>hello world</div>;

原生DOM相关

原生的 DOM 相关的类型,主要有以下这么几个:Element、 HTMLElement、HTMLxxxElment。

简单来说: Element = HTMLElement + SVGElement。

SVGElement一般开发比较少用到,而HTMLElement却非常常见,它的子类型包括HTMLDivElement、HTMLInputElement、HTMLSpanElement等等。

因此我们可以得知,其关系为:Element > HTMLElement > HTMLxxxElement,原则上是尽量写详细。

类组件

// Second.tsx

import * as React from 'react'
import SecondComponent from './component/Second1'
export interface ISecondProps {}

export interface ISecondState {
  count: number
  title: string
}

export default class Second extends React.Component<
  ISecondProps,
  ISecondState
> {
  constructor(props: ISecondProps) {
    super(props)

    this.state = {
      count: 0,
      title: 'Second标题',
    }
    this.changeCount = this.changeCount.bind(this)
  }
  changeCount() {
    let result = this.state.count + 1
    this.setState({
      count: result,
    })
  }
  public render() {
    return (
      <div>
        {this.state.title}--{this.state.count}
        <button onClick={this.changeCount}>点击增加</button>
        <SecondComponent count={this.state.count}></SecondComponent>
      </div>
    )
  }
}
// second1.tsx

import * as React from 'react'

export interface ISecond1Props {
  count: number
}

export interface ISecond1State {
  title: string
}

export default class Second1 extends React.Component<
  ISecond1Props,
  ISecond1State
> {
  constructor(props: ISecond1Props) {
    super(props)

    this.state = {
      title: '子组件标题',
    }
  }

  public render() {
    return (
      <div>
        {this.state.title}---{this.props.count}
      </div>
    )
  }
}

函数组件

// Home.tsx

import * as React from 'react'
import { useState, useEffect } from 'react'
import Home1 from './component/Home1'
interface IHomeProps {
  childcount: number;
}

const Home: React.FC<IHomeProps> = (props) => {
  const [count, setCount] = useState<number>(0)
  function addcount() {
    setCount(count + 1)
  }
  return (
    <div>
      <span>Home父组件内容数字是{count}</span>
      <button onClick={addcount}>点击增加数字</button>
      <Home1 childcount={count}></Home1>
    </div>
  )
}

export default Home
// Home1.tsx

import * as React from 'react'

interface IHome1Props {
  childcount: number;
}

const Home1: React.FC<IHome1Props> = (props) => {
  const { childcount } = props
  return <div>Home组件1--{childcount}</div>
}

export default Home1
import React from 'react'
 
interface Props {
  name: string;
  color: string;
}
 
type OtherProps = {
  name: string;
  color: string;
}
 
// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
  return <h1>My Website Heading</h1>
}
 
// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
  <h1>My Website Heading</h1>

关于 interface 或 type ,我们建议遵循 react-typescript-cheatsheet 社区提出的准则:

  • 在编写库或第三方环境类型定义时,始终将 interface 用于公共 API 的定义。
  • 考虑为你的 React 组件的 State 和 Props 使用 type ,因为它更受约束。”

让我们再看一个示例:

import React from 'react'
 
type Props = {
   /** color to use for the background */
  color?: string;
   /** standard children prop: accepts any valid React Node */
  children: React.ReactNode;
   /** callback function passed to the onClick handler*/
  onClick: ()  => void;
}
 
const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
   return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}

在此 <Button /> 组件中,我们为 Props 使用 type。每个 Props 上方都有简短的说明,以为其他开发人员提供更多背景信息。? 表示 Props 是可选的。children props 是一个 React.ReactNode 表示它还是一个 React 组件。

通常,在 React 和 TypeScript 项目中编写 Props 时,请记住以下几点:

  • 始终使用 TSDoc 标记为你的 Props 添加描述性注释 /** comment */。
  • 无论你为组件 Props 使用 type 还是 interfaces ,都应始终使用它们。
  • 如果 props 是可选的,请适当处理或使用默认值。

与hooks的结合

在hooks中,并非全部钩子都与TS有强关联,比如useEffect就不依赖TS做类型定义,我们挑选比较常见的几个和TS强关联的钩子来看看。

// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')

TypeScript 推断出 useState 钩子给出的值。这是一个 React 和 TypeScript 协同工作的成果。

useState

在极少数情况下,你需要使用一个空值初始化 Hook ,可以使用泛型并传递联合以正确键入 Hook 。查看此实例:

type User = {
  email: string;
  id: string;
}
 
// the generic is the < >
// the union is the User | null
// together, TypeScript knows, "Ah, user can be User or null".
const [user, setUser] = useState<User | null>(null);

1、如果初始值能说明类型,就不用给 useState 指明泛型变量; 

// ❌这样写是不必要的,因为初始值0已经能说明count类型
const [count, setCount] = useState<number>(0);

// ✅这样写好点
const [count, setCount] = useState(0);

2、如果初始值是 null 或 undefined,那就要通过泛型手动传入你期望的类型,并在访问属性的时候通过可选链来规避语法错误。 

interface IUser {
  name: string;
  age: number;
}

const [user, setUser] = React.useState<IUser | null>(null);

console.log(user?.name);
import React, {useState, useEffect} from "react"

interface User{
  id: number;
  name: string;
  email: string;
}

const App: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users").then(response => response.json()).then(data => setUsers(data));
  }, [])


  return <div>
    <h2>用户列表</h2>
    <ul>
      {users.map(user => (<li key={user.id}>{user.name}-{user.email}</li>))}
    </ul>
  </div>
}

export default App;

userReducer

下面是一个使用 userReducer 的例子:

type AppState = {};
type Action =
  | { type: "SET_ONE"; payload: string }
  | { type: "SET_TWO"; payload: number };
 
export function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "SET_ONE":
      return {
        ...state,
        one: action.payload // `payload` is string
      };
    case "SET_TWO":
      return {
        ...state,
        two: action.payload // `payload` is number
      };
    default:
      return state;
  }
}

useRef

// App.tsx
import React, { useRef } from "react";
import "./App.css";

type AppProps = {
  message: string;
};

const App: React.FC<AppProps> = ({ message }) => {
  const myRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <input ref={myRef} />
      <button
        onClick={() => console.log((myRef.current as HTMLInputElement).value)}
      >
        {message}
      </button>
    </div>
  );
};

export default App;

// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App message="click"/>
  </React.StrictMode>
);

reportWebVitals();

可见,Hooks 并没有为 React 和 TypeScript 项目增加太多复杂性。

自定义 hook

如果我们需要仿照 useState 的形式,返回一个数组出去,则需要在返回值的末尾使用as const,标记这个返回值是个常量,否则返回的值将被推断成联合类型。

const useInfo = () => {
  const [age, setAge] = useState(0);

  return [age, setAge] as const; // 类型为一个元组,[number, React.Dispatch<React.SetStateAction<number>>]
};

React合成事件相关

在 React 中,原生事件被处理成了React 事件,其内部是通过事件委托来优化内存,减少DOM事件绑定的。言归正传,React 事件的通用格式为[xxx]Event,常见的有MouseEvent、ChangeEvent、TouchEvent,是一个泛型类型,泛型变量为触发该事件的 DOM 元素类型。

最常见的情况之一是 onChange 在表单的输入字段上正确键入使用的。这是一个例子:

import React from 'react'
 
const MyInput = () => {
  const [value, setValue] = React.useState('')
 
  // 事件类型是“ChangeEvent”
  // 我们将 “HTMLInputElement” 传递给 input
  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setValue(e.target.value)
  }
 
  return <input value={value} onChange={onChange} id="input-example"/>
}
// input输入框输入文字
const handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
  console.log(evt);
};

// button按钮点击
const handleButtonClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
  console.log(evt);
};

// 移动端触摸div
const handleDivTouch = (evt: React.TouchEvent<HTMLDivElement>) => {
  console.log(evt);
};

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>过渡事件对象

先处理onClick事件。React 提供了一个 MouseEvent 类型,可以直接使用:

import { 
    useState, 
    MouseEvent,
} from 'react';

export default function App() {
    
  // 省略部分代码
  
  const handleClick = (event: MouseEvent) => {
    console.log('提交被触发');
  };

  return (
    <div className="App">      
      <button onClick={handleClick}>提交</button>
    </div>
  );
}

 onClick 事件实际上是由React维护的:它是一个合成事件。
合成事件是React对浏览器事件的一种包装,以便不同的浏览器,都有相同的API。

handleInputChange函数与 handleClick 非常相似,但有一个明显的区别。不同的是,ChangeEvent 是一个泛型,你必须提供什么样的DOM元素正在被使用。 

import { 
    useState, 
    ChangeEvent
} from 'react';

export default function App() {
  const [inputValue, setInputValue] = useState('');

  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value);
  };

  // 省略部分代码

  return (
    <div className="App">
      <input value={inputValue} onChange={handleInputChange} />
    </div>
  );
}

 在上面的代码中需要注意的一点是,HTMLInputElement 特指HTML的输入标签。如果我们使用的是 textarea,我们将使用 HTMLTextAreaElement 来代替。

注意,MouseEvent 也是一个泛型,你可以在必要时对它进行限制。例如,让我们把上面的 MouseEvent 限制为专门从一个按钮发出的鼠标事件。

const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
  console.log('提交被触发');
};

styles

// utils.d.ts
declare interface StyleProps {
  style?: React.CSSProperties
  className?: string
}
// Button.tsx
interface ButtonProps extends StyleProps {
  label: string
}
const Button = ({ label, ...styleProps }: ButtonProps) => (
  <button {...styleProps}>{label}</button>
)

扩展组件的 Props

有时,您希望获取为一个组件声明的 Props,并对它们进行扩展,以便在另一个组件上使用它们。但是你可能想要修改一两个属性。还记得我们如何看待两种类型组件 Props、type 或 interfaces 的方法吗?取决于你使用的组件决定了你如何扩展组件 Props 。让我们先看看如何使用 type:

import React from 'react';
 
type ButtonProps = {
    /** the background color of the button */
    color: string;
    /** the text to show inside the button */
    text: string;
}
 
type ContainerProps = ButtonProps & {
    /** the height of the container (value used with 'px') */
    height: number;
}
 
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

如果你使用 interface 来声明 props,那么我们可以使用关键字 extends 从本质上“扩展”该接口,但要进行一些修改:

import React from 'react';
 
interface ButtonProps {
    /** the background color of the button */
    color: string;
    /** the text to show inside the button */
    text: string;
}
 
interface ContainerProps extends ButtonProps {
    /** the height of the container (value used with 'px') */
    height: number;
}
 
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

两种方法都可以解决问题。由您决定使用哪个。就个人而言,扩展 interface 更具可读性,但最终取决于你和你的团队。

redux相关

对于action的定义,我们可以使用官方暴露的AnyAction,放宽对于action内部键值对的限制,如下:

import { AnyAction } from "redux";

const DEF_STATE = {
  count: 0,
  type: 'integer'
};

// 使用redux的AnyAction放宽限制
function countReducer(state = DEF_STATE, action: AnyAction) {
  switch (action.type) {
    case "INCREASE_COUNT":
      return {
        ...state,
        count: state.count + 1,
      };
    case "DECREASE_COUNT":
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
}

export default countReducer;

第三方库

无论是用于诸如 Apollo 之类的 GraphQL 客户端还是用于诸如 React Testing Library 之类的测试,我们经常会在 React 和 TypeScript 项目中使用第三方库。发生这种情况时,你要做的第一件事就是查看这个库是否有一个带有 TypeScript 类型定义 @types 包。你可以通过运行:

#yarn
yarn add @types/<package-name>
 
#npm
npm install @types/<package-name>

例如,如果您使用的是 Jest ,则可以通过运行以下命令来实现:

#yarn
yarn add @types/jest
 
#npm
npm install @types/jest

这样,每当在项目中使用 Jest 时,就可以增加类型安全性。

该 @types 命名空间被保留用于包类型定义。它们位于一个名为 DefinitelyTyped 的存储库中,该存储库由 TypeScript 团队和社区共同维护。

规约

子组件的入参命名为[组件名]Props,如:

// 比如当前组件名为InfoCard
export interface InfoCardProps {
  name: string;
  age: number;
}

2、interface接口类型以大写开头;

3、为后端接口的出入参书写interface,同时使用利于编辑器提示的jsdoc风格做注释,如:

export interface GetUserInfoReqParams {
    /** 名字 */
    name: string;
    /** 年龄 */
    age: number;
    /** 性别 */
    gender: string;
}

其他

键名或键值不确定如何处理?

// 表示键名不确定,键值限制为number类型
export interface NotSureAboutKey {
  [key: string]: number;
}

// 当键名键值都不确定时,以下接口对任何对象都是适用的
export interface AllNotSure {
  [key: string]: any;
}

如何在接口中使用泛型变量?

所谓泛型,就是预定义类型。它的目的是:达到类型定义的局部灵活,提高复用性。我们通常会在接口中使用泛型,如:

// 通常,我们会为接口的泛型变量指定一个默认类型
interface IHuman<T = unknown> {
  name: string;
  age: number;
  gender: T;
}

// 其他地方使用时
const youngMan: IHuman<string> = {
    name: 'zhangsan',
    age: 18,
    gender: 'male'
}

NodeListOf使用:

  const onChangeWidth = () => {
    const elements = document.querySelectorAll(".menu-item dd") as NodeListOf<
      HTMLElement
    >;

    if (status) {
      mobxData.setWidth(90);

      for (let i = 0; i < elements.length; i++) {
        (elements[i].style as any).display = "block";
      }
    } else {
      mobxData.setWidth(60);

      for (let i = 0; i < elements.length; i++) {
        (elements[i].style as any).display = "none";
      }
    }

    setStatus(!status);
  };

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值