目录

前言

组件 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'
}