跳至主要內容

React的TS开发踩坑指南

zfh大约 57 分钟约 16959 字...

创建 react-ts 项目

npx create-react-app my-app --template typescript

React

函数式组件 props 类型定义

interface PropsType {
  title: string | JSX.Element
  sideImage: string
  products: any[]
  color: string
}
const Recommendation: React.FC<PropsType> = function ({title,sideImage,products,color}) {
    return (
         // ...
    )
}

 





Redux ToolKit

获取 state 的类型

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './rootReducer'
const store = configureStore({
  reducer: rootReducer,
})
export type RootState = ReturnType<typeof store.getState>


 



returnType用于获取函数返回值类型,typeof用于获取函数的类型

获取 dispatch 的类型

export type AppDispatch = typeof store.dispatch

定义 aciton 参数的类型

使用 PayloadAction 类型来声明 action.payload:

    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },

react-redux

虽然可以将RootStateAppDispatch类型导入到每个组件中,但最好创建预先定义好类型的useDispatchanduseSelector挂钩以便在您的应用程序中使用。这很重要,原因有几个:

  • 对于useSelector,它使您无需每次都输入(state: RootState)
  • 对于useDispatch,默认的Dispatch类型不包含 thunk 或其他中间件
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'

export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
// hoooks/useRedux.ts

react-router

路由配置文件

在使用react router v6单独抽离路由配置文件时,这个文件应该是tsx,而不是ts

Style-Component

传参时定义类型

interface styledProps {
  color: string
}

const Title = styled.span<styledProps>`
  color: ${(props) => props.color};
  font-size: 32px;
`

提示

下面的内容为系统性总结,转载自:在React中拥抱TypeScriptopen in new window

一、组件声明

在React中,组件的声明方式有两种:函数组件类组件, 来看看这两种类型的组件声明时是如何定义TS类型的。

1. 类组件

类组件的定义形式有两种:React.Component<P,S>React.PureComponent<P,S,SS>,它们都是泛型接口,接收两个参数,第一个是props类型的定义,第二个是state类型的定义,这两个参数都不是必须的,没有时可以省略:

//App.tsx
import React from "react";

interface IProps {
  name: string;
}

interface IState {
  count: number;
}

class App extends React.Component<IProps, IState> {
  state = {
    count: 0,
  };

  render() {
    return (
      <div>
        {this.state.count}
        {this.props.name}
      </div>
    );
  }
}

export default App;

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App name="qq" />
  </React.StrictMode>,
  document.getElementById("root")
);

React.PureComponent<P,S,SS> 也是差不多的:

class App extends React.PureComponent<IProps, IState> {}

React.PureComponent是有第三个参数的,它表示getSnapshotBeforeUpdate的返回值。

那PureComponent和Component 的区别是什么呢?

它们的主要区别是PureComponent中的shouldComponentUpdate 是由自身进行处理的,不需要我们自己处理,所以PureComponent可以在一定程度上提升性能。

有时候可能会见到这种写法,实际上和上面的效果是一样的:

import React, {PureComponent, Component} from "react";

class App extends PureComponent<IProps, IState> {}

class App extends Component<IProps, IState> {}

那如果定义时候我们不知道组件的props的类型,只有在调用时才知道组件类型,该怎么办呢?这时泛型就发挥作用了:

//App.tsx
import React from "react";
//导入类型IProps
import {type IProps} from './index'
//泛型继承导入的类型
 class MyComponent<P extends IProps> extends React.Component<P> {
  internalProp: P;
  constructor(props: P) {
    super(props);
    this.internalProp = props;
  }

  render() {
    console.log(this.props);
    console.log(this.internalProp);

    const { age ,name} = this.props;
    return (
      <div>
        {age}
        {name}
        <span>hello world </span>;
      </div>
    );
  }
};
export default MyComponent;

//index.tsx

import React from "react";
import ReactDOM from "react-dom";
import MyComponent from "./App";
//声明类型并导出
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <MyComponent<IProps> name="React" age={18} />   // Success
    <MyComponent<IProps> name="TypeScript" age="hello" />; // Error 
  </React.StrictMode>,
  document.getElementById("root")
);

2. 函数组件

通常情况下,函数组件我是这样写的:

interface IProps {
  name: string
}

const App = (props: IProps) => {
  const {name} = props;

  return (
    <div className="App">
      <h1>hello world</h1>
      <h2>{name}</h2>
    </div>
  );
}

export default App;

除此之外,函数类型还可以使用React.FunctionComponent<P={}>来定义,也可以使用其简写React.FC<P={}>,两者效果是一样的。它是一个泛型接口,可以接收一个参数,参数表示props的类型,这个参数不是必须的。它们就相当于这样:

type React.FC<P = {}> = React.FunctionComponent<P>

最终的定义形式如下

//app.tsx
import React from "react";
interface IProps {
  name: string
}

const App: React.FC<IProps> = (props) => {
  const {name} = props;
  return (
    <div className="App">
      <h1>hello world</h1>
      <h2>{name}</h2>
    </div>
  );
}
export default App;

//index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App name="qq" />
  </React.StrictMode>,
  document.getElementById("root")
);


当使用这种形式来定义函数组件时,props中默认会带有children属性,它表示该组件在调用时,其内部的元素。

完整案例

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <App name="zgc" />
  </React.StrictMode>,
  document.getElementById("root")
);

//App.tsx
import React from "react";
import Child1 from "./child1";
import Child2 from "./child2";

interface IProps {
  name: string;
}
const App: React.FC<IProps> = (props) => {
  const { name } = props;
  return (
    <Child1 name={name}>
      <Child2 name={name} />
      TypeScript
    </Child1>
  );
};
export default App;

//child1.tsx
interface IProps {
  name: string;
}
const Child1: React.FC<IProps> = (props) => {
  const { name, children } = props;
   //我个人更喜欢使用 `React.FC` 的方式来创建有类型约束的函数式组件,它还支持 `children` 的传入,即使在我们的类型中并没有定义它
  console.log("child1", name, children);
  return (
    <div>
      <h1>hello child1</h1>
      <h2>{name}</h2>
      {children} //展示的内容为下图后三行
    </div>
  );
};
export default Child1;

//child2.tsx
interface IProps {
  name: string;
}
const Child2: React.FC<IProps> = (props) => {
  const { name } = props;
  return (
    <div>
      <h1>hello child2</h1>
      <h2>{name}</h2>
    </div>
  );
};
export default Child2;

1639563264(1).png 使用 React.FC 声明函数组件和普通声明的区别如下:

  • React.FC 显式地定义了返回类型,其他方式是隐式推导的;
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;
  • React.FC 为 children 提供了隐式的类型(ReactElement | null)。

那如果我们在定义组件时不知道props的类型,只有调用时才知道,那就还是用泛型来定义props的类型。对于使用function定义的函数组件:

//App.tsx
import {type IProps} from './index'
// 定义组件
function MyComponent<P extends IProps >(props: P) {
  const {name ,age } = props
  return <span>{name} {age}</span>;
}
export default MyComponent;

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import MyComponent from "./App";
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <MyComponent<IProps> name="React" age={18} />; // Success
    <MyComponent<IProps> name="TypeScript" age="hello" />; // Error 
  </React.StrictMode>,
  document.getElementById("root")
);

使用箭头函数定义的函数组件

//App.tsx
import {type IProps} from './index'
// 定义组件
const MyComponent = <P extends IProps >(props: P) =>{
  const {name ,age } = props
  return <span>{name} {age}</span>;
}
export default MyComponent;

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import MyComponent from "./App";
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <MyComponent<IProps> name="React" age={18} />; // Success
    {/* <MyComponent<IProps> name="TypeScript" age="hello" />; // Error */}
  </React.StrictMode>,
  document.getElementById("root")
);

在通常情况下,使用 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>

二、React内置类型

1. JSX.Element

先来看看JSX.Element类型的声明:

declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> { }
  }
}

可以看到,JSX.Element是ReactElement的子类型,它没有增加属性,两者是等价的。也就是说两种类型的变量可以相互赋值。

JSX.Element 可以通过执行 React.createElement 或是转译 JSX 获得:

const jsx = <div>hello</div>
const ele = React.createElement("div", null, "hello");

2. React.ReactElement

React 的类型声明文件中提供了 React.ReactElement<T>,它可以让我们通过传入<T/>来注解类组件的实例化,它在声明文件中的定义如下:

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
   type: T;
   props: P;
   key: Key | null;
}

ReactElement是一个接口,包含type,props,key三个属性值。该类型的变量值只能是两种: null 和 ReactElement实例。

通常情况下,函数组件返回ReactElement(JXS.Element)的值。

3. React.ReactNode

ReactNode类型的声明如下:

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

可以看到,ReactNode是一个联合类型,它可以是string、number、ReactElement、null、boolean、ReactNodeArray。由此可知。ReactElement类型的变量可以直接赋值给ReactNode类型的变量,但反过来是不行的。

类组件的render 成员函数会返回 ReactNode 类型的值:

class MyComponent extends React.Component {
	render() {
    	return <div>hello world</div>
    }
}
// 正确
const component: React.ReactNode<MyComponent> = <MyComponent />;
// 错误
const component: React.ReactNode<MyComponent> = <OtherComponent />;

上面的代码中,给component变量设置了类型是Mycomponent类型的react实例,这时只能给其赋值其为MyComponent的实例组件。

通常情况下,类组件通过 render() 返回 ReactNode的值。

4. React Prop 类型

  • 如果你有配置 Eslint 等一些代码检查时,一般函数组件需要你定义返回的类型,或传入一些 React 相关的类型属性。

这时了解一些 React 自定义暴露出的类型就很有必要了。例如常用的 React.ReactNode

export declare interface AppProps {
    children1: JSX.Element; // ❌ bad, 没有考虑数组类型
    children2: JSX.Element | JSX.Element[]; // ❌ 没考虑字符类型
    children3: React.ReactChildren; // ❌ 名字唬人,工具类型,慎用
    children4: React.ReactChild[]; // better, 但没考虑 null
    children: React.ReactNode; // ✅ best, 最佳接收所有 children 类型
    functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点
    
    style?: React.CSSProperties; // React style
    
    onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型
}

三、React Hooks

如果对React Hooks还不熟悉的同学可以看我之前写的文章: React Hooks 全解open in new window

1. useState

默认情况下,React会为根据设置的state的初始值来自动推导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'])

如果已知state 的类型,可以通过以下形式来自定义state的类型:

const [count, setCount] = useState<number>(1)

  type ArticleInfo = {
    title: string;
    content: number;
  };

  const [article, setArticle] = useState<ArticleInfo>({ title:"zgc", content:1 });

如果初始值为null,需要显式地声明 state 的类型:

const [count, setCount] = useState<number | null>(null); 

下面是声明文件中 useState 的定义:

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
// convenience overload when first argument is omitted
	/**
	 * Returns a stateful value, and a function to update it.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#usestate
   */
    
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
  /**
   * An alternative to `useState`.
   *
   * `useReducer` is usually preferable to `useState` when you have complex state logic that involves
   * multiple sub-values. It also lets you optimize performance for components that trigger deep
   * updates because you can pass `dispatch` down instead of callbacks.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#usereducer
   */

可以看到,这里定义两种形式,分别是有初始值和没有初始值的形式。

2. useEffect

你可以把 useEffect 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。 useEffect的主要作用就是处理副作用,它的第一个参数是一个函数,表示要清除副作用的操作,第二个参数是一组值,当这组值改变时,第一个参数的函数才会执行,这让我们可以控制何时运行函数来处理副作用:

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source]
);

当函数的返回值不是函数或者effect函数中未定义的内容时,如下:

useEffect(
    () => {
      subscribe();
      return null; 
    }
);

TypeScript就会报错:

image.png
image.png

来看看useEffect在类型声明文件中的定义:

// Destructors are only allowed to return void.
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };

// NOTE: callbacks are _only_ allowed to return either void, or a destructor.
type EffectCallback = () => (void | Destructor);

// TODO (TypeScript 3.0): ReadonlyArray<unknown>
type DependencyList = ReadonlyArray<any>;

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
// NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref<T>
  /**
   * `useImperativeHandle` customizes the instance value that is exposed to parent components when using
   * `ref`. As always, imperative code using refs should be avoided in most cases.
   *
   * `useImperativeHandle` should be used with `React.forwardRef`.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle
   */

可以看到,useEffect的第一个参数只允许返回一个函数,或者是undefined,其他情况都会报错。

案例

比较常见的一个情况是,我们的 useEffect 需要执行一个 async 函数,比如:

// ❌ 
// Type 'Promise<void>' provides no match 
// for the signature '(): void | undefined'
useEffect(async () => {
  const user = await getUser()
  setUser(user)
}, [])

虽然没有在 async 函数里显式的返回值,但是 async 函数默认会返回一个 Promise,这会导致 TS 的报错

正解

 // 获取后端数据
  useEffect(() => {
    // 防止内存泄露
    let isUnmount = false;
    const dataList = async () => {
      const {data: { 项目col: col }} = await getLists();
      if (!isUnmount) {
        setData(col);
      }
    };
    dataList();
    return () => { //返回值为函数
      isUnmount = true;
    };
  }, []);

3. useRef

当初始值为 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
}, [])

当使用 useRef 时,我们可以访问一个可变的引用对象。可以将初始值传递给 useRef,它用于初始化可变 ref 对象公开的当前属性。当我们使用useRef时,需要给其指定类型:

import React from "react";

export default function App() {
  // 初始化为 null, 但告知 TS 是希望 HTMLInputElement 类型
  // inputEl 只能用于 input elements
  const inputEl = React.useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    // TS 会检查 inputEl 类型,初始化 null 是没有 current 上是没有 focus 属性的
    // 你需要自定义判断!
    if (inputEl && inputEl.current) {
      inputEl.current.focus();
    }
    //或者: ✅ best
    inputEl.current?.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}


onButtonClick 事件触发时,可以肯定 inputEl 也是有值的,因为组件是同级别渲染的,但是还是依然要做冗余的非空判断。

有一种办法可以绕过去。

const ref1 = useRef<HTMLElement>(null!);

null! 这种语法是非空断言,跟在一个值后面表示你断定它是有值的,所以在你使用 inputEl.current.focus() 的时候,TS 不会给出报错。

但是这种语法比较危险,需要尽量减少使用。

在绝大部分情况下,inputEl.current?.focus() 是个更安全的选择,除非这个值真的不可能为空。(比如在使用之前就赋值了)

4. useCallback

先来看看类型声明文件中对useCallback的定义:

 function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
 /**
  * `useMemo` will only recompute the memoized value when one of the `deps` has changed.
  *
  * Usage note: if calling `useMemo` with a referentially stable function, also give it as the input in
  * the second argument.
  *
  * ```ts
  * function expensive () { ... }
  *
  * function Component () {
  *   const expensiveResult = useMemo(expensive, [expensive])
  *   return ...
  * }
  * ```
  *
  * @version 16.8.0
  * @see https://reactjs.org/docs/hooks-reference.html#usememo
  */

useCallback接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生变化时才会重新执行回调函数。来看一个例子:

import { useCallback, useState } from "react";
function App() {
  const add = (a: number, b: number) => console.log(a + b);

  const [b, setb] = useState<number>();
  const memoizedCallback = useCallback(
    (a, b) => {
      add(a, b);
    },
    [b]
  );
 
  return <div>1</div>;
}
export default App;

这里我们没有给回调函数中的参数a定义类型,所以下面的调用方式都不会报错:

 memoizedCallback("hello", 1); //hello1
  memoizedCallback(5, 1);   //6

尽管add方法的两个参数都是number类型,但是上述调用都能够用执行。所以为了更加严谨,我们需要给回调函数定义具体的类型:

const memoizedCallback = useCallback(
    (a: number, b: number) => {
      add(a, b);
    },
    [b]
  );

这时候如果再给回调函数传入字符串就会报错了:

image.png
image.png

5. useMemo

先来看看类型声明文件中对useMemo的定义:

function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
   /**
    * `useDebugValue` can be used to display a label for custom hooks in React DevTools.
    *
    * NOTE: We don’t recommend adding debug values to every custom hook.
    * It’s most valuable for custom hooks that are part of shared libraries.
    *
    * @version 16.8.0
    * @see https://reactjs.org/docs/hooks-reference.html#usedebugvalue
    */

useMemo和useCallback是非常类似的,但是它返回的是一个值,而不是函数。所以在定义useMemo时需要定义返回值的类型:

import { useMemo } from "react";
function App() {

  let a = 1;
  setTimeout(() => {
    a += 1;
  }, 1000);

  const calculatedValue = useMemo<number>(() => a ** 2, [a]);
}
export default App;

如果返回值不一致,就会报错:

const calculatedValue = useMemo<number>(() => a + "hello", [a]);
// 类型“() => string”的参数不能赋给类型“() => number”的参数

注:useMemo 的泛型指定了返回值类型,useCallback 的泛型指定了参数类型

// 也可以显式的指定返回值类型,返回值不一致会报错

const result = React.useMemo<string>(() => 2, [])

// 类型“() => number”的参数不能赋给类型“() => string”的参数。

const handleChange = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(
      evt => {
           console.log(evt.target.value)
      }, []
)

6. useReducer

有时我们需要处理一些复杂的状态,并且可能取决于之前的状态。这时候就可以使用useReducer,它接收一个函数,这个函数会根据之前的状态来计算一个新的state。其语法如下:

const [state, dispatch] = useReducer(reducer, initialArg, init);

案例1

import { useReducer } from "react";
type ActionType = {
  type: "increment" | "decrement";
};

type State = { count: number };

function reducer(state: State, action: ActionType) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const initialState = { count: 0 };
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

export default Counter;

这样,在Counter函数中就可以推断出类型。当我们视图使用一个不存在的类型时,就会报错:

dispatch({type: 'reset'});
// Error! type '"reset"' is not assignable to type '"increment" | "decrement"'

除此之外,还可以使用泛型的形式来实现reducer函数的类型定义:

type ActionType = {
  type: 'increment' | 'decrement';
};

type State = { count: number };

const reducer: React.Reducer<State, ActionType> = (state, action) => {
  // ...
}

泛型写法

import React, { useReducer } from "react";

type ActionType = {
  type: "increment" | "decrement";
};

type State = { count: number };

const Counter: React.FC = () => {
//const reducer = (state: State, action: ActionType) => {
  const reducer: React.Reducer<State, ActionType> = (state, action) => {
    switch (action.type) {
      case "increment":
        return { count: state.count + 1 };
      case "decrement":
        return { count: state.count - 1 };
      default:
        throw new Error();
    }
  };

  const initialState: State = {count: 0}
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
};
export default Counter;

案例2

import { useReducer } from "react";
// ❌ bad,可能传入未定义的 type 类型,或码错单词,而且还需要针对不同的 type 来兼容 payload
// type ACTIONTYPE = { type: string; payload?: number | string };

// ✅ good
type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string }
  | { type: "initial" };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - Number(action.payload) };
    case "initial":
      return { count: initialState.count };
    default:
      throw new Error();
  }
}
const initialState = { count: 0 };

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}
export default Counter;

「Discriminated Unions」一般是一个联合类型,其中每一个类型都需要通过类似 type 这种特定的字段来区分,当你传入特定的 type 时,剩下的类型 payload 就会自动匹配推断。

这样:

  • 当你写入的 type 匹配到 decrement 的时候,TS 会自动推断出相应的 payload 应该是 string 类型。
  • 当你写入的 type 匹配到 increment 的时候,则 payload 应该是 number 类型。

这样在你 dispatch 的时候,输入对应的 type,就自动提示你剩余的参数类型啦。

7. useContext

useContext需要提供一个上下文对象,并返回所提供的上下文的值,当提供者更新上下文对象时,引用这些上下文对象的组件就会重新渲染:

案例1

import React, { useContext } from "react";

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee",
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222",
  },
};

const ThemeContext = React.createContext(themes.light);
//themes.light初始默认值
function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
//接收传递过来的themes.dark
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}
export default App;

在使用useContext时,会自动推断出提供的上下文对象的类型,所以并不需要我们手动设置context的类型。当前,我们也可以使用泛型来设置context的类型:

interface Ithemes {
  foreground: string;
  background: string;
}

const ThemeContext = React.createContext<Ithemes>(themes.light);

下面是useContext在类型声明文件中的定义:

function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T;
/**
  * Returns a stateful value, and a function to update it.
  *
  * @version 16.8.0
  * @see https://reactjs.org/docs/hooks-reference.html#usestate
  */

案例2useContextuseReducer 结合使用,来管理全局的数据流

import React, { useContext, useReducer } from "react";
type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string }
  | { type: "initial" };

interface AppContextInterface {
  state: typeof initialState;
  dispatch: React.Dispatch<ACTIONTYPE>;
}

const initialState = { count: 0 };

const AppCtx = React.createContext<AppContextInterface>({
  state: initialState,
  dispatch: (action) => action,
});

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - Number(action.payload) };
    case "initial":
      return { count: initialState.count };
    default:
      throw new Error();
  }
}

const App = (): JSX.Element => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <AppCtx.Provider value={{ state, dispatch }}>
      <Counter />
    </AppCtx.Provider>
  );
};

function Counter() {
  const { state, dispatch } = useContext(AppCtx);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}

export default App;

8. 自定义 Hooks

Hooks 的美妙之处不只有减小代码行的功效,重点在于能够做到逻辑与 UI 分离。做纯粹的逻辑层复用。

export function useLoading() {
    const [isLoading, setState] = React.useState(false);
    const load = (aPromise: Promise<any>) => {
        setState(true);
        return aPromise.finally(() => setState(false));
    };
    return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联合类型 (boolean | typeof load)[]
}

  • 也可以断言成 tuple type 元组类型。
export function useLoading() {
    const [isLoading, setState] = React.useState(false);
    const load = (aPromise: Promise<any>) => {
        setState(true);
        return aPromise.finally(() => setState(false));
    };
    return [isLoading, load] as [
        boolean, 
        (aPromise: Promise<any>) => Promise<any>
    ];
}

  • 如果对这种需求比较多,每个都写一遍比较麻烦,可以利用泛型定义一个辅助函数,且利用 TS 自动推断能力。
function tuplify<T extends any[]>(...elements: T) {
    return elements;
}

function useArray() {
    const numberValue = useRef(3).current;
    const functionValue = useRef(() => {}).current;
    return [numberValue, functionValue]; // type is (number | (() => void))[]
}

function useTuple() {
    const numberValue = useRef(3).current;
    const functionValue = useRef(() => {
    }).current;
    return tuplify(numberValue, functionValue); // type is [number, () => void]
}

四、事件处理

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元素的类型,这个类型就是我们绑定这个事件的标签元素的类型,标签元素类型将在下面的第五部分介绍。

来看一个简单的例子:

import { useState } from "react";

const App: React.FC = () => {
  const [text, setText] = useState<string>("");

  const onChange = (e: React.FormEvent<HTMLInputElement>): void => {
    // console.log("e", e);
    // console.log("e.currentTarget", e.currentTarget);
    // console.log("e.target", e.target);
    // console.log(e.currentTarget === e.target); //true,在此他们相等
    // e.currentTarget总是指向事件绑定的元素,而e.target 则是事件触发的元素。
    setText(e.currentTarget.value);
  };
  // console.log("text", text);
  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
    </div>
  );
};

export default App;

1639626252(1).png
1639626252(1).png

这里就给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。来看下面的例子:

import { useState } from "react";

const App: React.FC = () => {
  const [SourceInput, setSourceInput] = useState<string>("");

  const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.value.length > 30) {
      console.log("请长度不能超过30个字,请重新输入");
      return;
    }
    setSourceInput(e.target.value);
  };
  console.log(SourceInput);

  return (
    <div>
      <input onChange={(e) => onSourceChange(e)} placeholder="最多30个字" />
    </div>
  );
};

export default App;

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

image.png
image.png

再来看一个例子

import React, { useState } from "react";

export default function App() {
  const [current, setCurrent] = useState<number>();
  const handleChangeCurrent = (
    item: number,
    e: React.MouseEvent<HTMLDivElement>
  ) => {
    e.stopPropagation(); //阻止事件冒泡,点击几就是几,否则返回的是最外层的值
    setCurrent(item);
  };
  console.log(current);

  return (
    <div
      onClick={(e) => handleChangeCurrent(1, e)}
      style={{
        width: "100px",
        height: "100px",
        backgroundColor: "red",
      }}
    >
      1
      <div
        onClick={(e) => handleChangeCurrent(2, e)}
        style={{
          width: "60px",
          height: "60px",
          backgroundColor: "blue",
        }}
      >
        2
        <div
          onClick={(e) => handleChangeCurrent(3, e)}
          style={{
            width: "30px",
            height: "30px",
            backgroundColor: "green",
          }}
        >
          3
        </div>
      </div>
    </div>
  );
}


1639644105(1).png
1639644105(1).png
import React, { useState } from "react";

export default function App() {
  const onClickInner = (e: React.MouseEvent<HTMLDivElement>) => {
    e.stopPropagation();
    console.log("inner div");
  };
  const onClickOuter = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log("outer div");
  };

  return (
    <div onClick={onClickOuter}>
      <div onClick={onClickInner}>inner div</div>
    </div>
  );
}


1639643732(1).png 这里的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>)=>{
    //... 
  }}
/>

import * as React from "react";

const App: React.FC = () => {
  const [state, setState] = React.useState("");

  const onChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
    setState(e.currentTarget.value);
  };

  return (
    <div>
      <input type="text" value={state} onChange={onChange} />
    </div>
  );
};
export default App;

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。

案例1

import React from "react";

export default function App() {
  const handleChangeCurrent: React.MouseEventHandler<HTMLDivElement> = (
    e: React.MouseEvent<HTMLDivElement>
  ) => {
    console.log(e.clientX);
  };
  return <div onClick={(e) => handleChangeCurrent(e)}>1</div>;
}

案例2

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>
  )
}
export default App

五、HTML标签类型

1. 常见标签类型

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

image.png
image.png

所有的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> 

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

六、工具泛型

在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。不过在具体介绍之前,我得先介绍一些相关的基础知识,方便你可以更好的学习其它的工具类型。

1. keyof typeof

在用 TypeScript 的时候,我们常会类似下面的例子一样写~

enum ColorsEnum {
    white = '#ffffff',
    black = '#000000',
}

type Colors = keyof typeof ColorsEnum;

其中最后一行等价于

type Colors = "white" | "black"

那么其中 keyof typeof 是如何工作的呢?下面我们开始分析~

想要理解 TypeScript 里 keyof typeof 是如何工作的,首先需要理解什么是 字面量类型(literal types)联合字面量类型(union of literal types) ,我将会先解释这些概念,然后分别详细介绍 keyoftypeof,最后会回到 enum 来回答上面的问题。这个答案有点长,但是例子都很容易理解。

字面量类型(literal types)

Typescript 中的字面量类型是更具体的 string, numberboolean 类型。比如 "Hello World" 是一个 string,但是 string 类型不是 "Hello World""Hello World"string 类型的一个更具体的类型,所以它是一个字面量类型。

一个字面量类型可以被这样定义:

type Greeting = "Hello"

这意味着 Greeting 类型的对象只能有一个字符串值 "Hello",并且没有其他 string 类型的值,或者其他任何类型的值,就像是下面代码说的一样:

let greeting: Greeting
greeting = "Hello" // OK
greeting = "Hi"    // Error: Type '"Hi"' is not assignable to type '"Hello"'

字面量类型本身并不是很有用,但是当它和联合类型(union types)、类型别名(type aliases)、类型保护(type guards)组合起来后,它就会变得很强大

下面是联合字面量类型的例子:

type Greeting = "Hello" | "Hi" | "Welcome"

现在 Greeting 类型对象的值可以是 "Hello", "Hi" 或者 "Welcome"

let greeting: Greeting
greeting = "Hello"       // OK
greeting = "Hi"          // OK
greeting = "Welcome"     // OK
greeting = "GoodEvening" // Error: Type '"GoodEvening"' is not assignable to type 'Greeting'

keyof 单独使用

假设现在有一个类型 Tkeyof T 将会给你一个新类型,它是我们前面提到的 联合字面量类型,并且组成它的字面量类型是 T 的属性名称。最后生成的类型是字符串的子类型。

比如来看下下面的 interface

interface Person {
    name: string
    age: number
    location: string
}

Person 类型上使用 keyof,将会得到一个新类型,如下面代码所示:

type SomeNewType = keyof Person

SomeNewType 是一个联合字面量类型("name" | "age" | "location"),它是由 Person 的属性组成的类型。

现在,你可以创建 SomeNewType 类型的对象了:

let newTypeObject: SomeNewType
newTypeObject = "name"           // OK
newTypeObject = "age"            // OK
newTypeObject = "location"       // OK
newTypeObject = "anyOtherValue"  // Error...Type '"anyOtherValue"' is not assignable to type 'keyof Person'

typeof 单独使用

TypeScript 添加的 typeof 方法可以在类型上下文(type context)中使用,用于获取一个变量或者属性的类型。

import React from "react";
export default function App() {
  interface Person {
    name: string;
    age: number;
  }

  const sem: Person = { name: "semlinker", age: 30 };

  return <div>1</div>;
}
//等价于

import React from "react";
export default function App() {
 
  const sem = { name: "semlinker", age: 30 };

  type Sem = typeof sem; // type Sem = Person
  const lolo: Sem = { name: "lolo", age: 5 };
  return <div>1</div>;
}


在上面代码中,我们通过 typeof 操作符获取 sem 变量的类型并赋值给 Sem 类型变量,之后我们就可以使用 Sem 类型

你也可以对嵌套对象执行相同的操作:

const kakuqo = {
    name: "kakuqo",
    age: 30,
    address: {
      province: '福建',
      city: '厦门'   
    }
}

type Kakuqo = typeof kakuqo;
//等价于
/*
 type Kakuqo = {
    name: string;
    age: number;
    address: {
        province: string;
        city: string;
    };
}
*/

此外,typeof 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:

function toArray(x: number): Array<number> {
return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

keyof typeof 同时使用

你可能已经知道,typeof 运算符为你提供对象的类型,上面例子中 Person interface,我们已经知道它的类型,所以我们只需要在 Person 上使用 keyof 操作符。

但是,当我们不知道对象的类型,或者我们只有一个值,类似于下面的情况,应该怎么办呢?

const bmw = { name: "BMW", power: "1000hp" }

这就是我们需要一起使用 keyof typeof 的地方。

typeof bmw 给到你他们的类型 { name: string, power: string }

接着 keyof 操作符给到你联合字面量类型,像下面代码描述的一样:

type CarLiteralType = keyof typeof bmw

let carPropertyLiteral: CarLiteralType
carPropertyLiteral = "name"       // OK
carPropertyLiteral = "power"      // OK
carPropertyLiteral = "anyOther"   // Error...Type '"anyOther"' is not assignable to type '"name" | "power"'

enum 上使用 keyof typeof

在 Typescript 中,enum 在编译时被用作类型,用来实现常量的类型安全,但是它们在运行时被视为对象。这是因为,当 Typescript 代码被编译为 Javascript 时,它们会被转换为普通对象。接着我们回顾一下,最开始我们提出问题的例子是这样的:

enum ColorsEnum {
    white = '#ffffff',
    black = '#000000',
}

这里 ColorsEnum 在运行时作为一个对象存在,不是一个类型,所以,我们需要一起使用 keyof typeof 这两个操作符,像下面代码展示的一样。

type Colors = keyof typeof ColorsEnum

let colorLiteral: Colors
colorLiteral = "white"  // OK
colorLiteral = "black"  // OK
colorLiteral = "red"    // Error...Type '"red"' is not assignable to type '"white" | "black"'

2. in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

3. Partial

Partial 作用是将传入的属性变为可选项。适用于对类型结构不明确的情况。它使用了两个关键字:keyof和in,先来看看他们都是什么含义。keyof 可以用来取得接口的所有 key 值:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }

  type T = keyof IPerson;
  
  const a: T = "age";
  const b: T = "name";
  const c: T = "height";

  return <div>1</div>;
}

// T 类型为: "name" | "age" | "height"

in关键字可以遍历枚举类型,:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }

  type T = keyof IPerson;
  // T 类型为: "name" | "age" | "height"
  type Obj = {
    [p in T]: any;
  };
  const a: Obj = {
    name: 1,
    age: 1,
    height: 1,
  };
  console.log(a);
  return <div>1</div>;
}

// Obj类型为: 
//type Obj = {
//name: any;
//age: any;
//height: any;
}

keyof 可以产生联合类型, in 可以遍历枚举类型, 所以经常一起使用, 下面是Partial工具泛型的定义

/**
 * Make all properties in T optional
 * 将T中的所有属性设置为可选
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

这里,keyof T 获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。中间的?就用来将属性设置为可选。

使用示例如下

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  // 少一个都不行
  const a: IPerson = {
    name: "zgc",
    age: 1,
    height: 1,
  };
  // 可选,无所谓少不少
  const b: Partial<IPerson> = {
    name: "zgc",
    age: 1,
    height: 1,
  };

  console.log(a, b);

  return <div>1</div>;
}

4. Required

Required 的作用是将传入的属性变为必选项,和上面的工具泛型恰好相反,其声明如下:

/**
 * Make all properties in T required
 * 将T中的所有属性设置为必选
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

可以看到,这里使用-?将属性设置为必选,可以理解为减去问号。适用形式和上面的Partial差不多:

import React from "react";

export default function App() {
  interface IPerson {
    name?: string;
    age?: number;
    height?: number;
  }
 // 可选,无所谓少不少
  const a: IPerson = {
    name: "zgc",
    age: 1,
    height: 1,
  };
  
   // 少一个都不行
  const b: Required<IPerson> = {
    name: "zgc",
    age: 1,
    height: 1,
  };

  console.log(a, b);

  return <div>1</div>;
}

5. Readonly

将T类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值。Readonly的声明形式如下:

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

使用示例如下:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  const person1: Readonly<IPerson> = {
    name: "zgc",
    age: 1,
    height: 1,
  };
  // person.age = 20; 不能修改 Error: cannot reassign a readonly property
  const person2: IPerson = {
    name: "zgc",
    age: 1,
    height: 1,
  };
  person2.age = 20; //能修改

  console.log(person1,person2);

  return <div>1</div>;
}

可以看到,通过 Readonly 将IPerson的属性转化成了只读,不能再进行赋值操作。

6. Extract<T, U>

选取 Type 类型和 Union 类型两者的公共部分并返回为一个新类型,可以理解为取交集

下面是其声明的形式:

/**

 * Extract from T those types that are assignable to U

 */

type Extract<T, U> = T extends U ? T : never;


使用示例如下:

export default function App() {
  type T0 = Extract<"a" | "b" | "c", "a">;
  // "a"
  type T1 = Extract<"a" | "b" | "c", "a" | "b">;
  // "a"|"b"
  type T2 = Extract<string | number | (() => void), Function>;
  // (() => void)

  const person: T0 = "a";

  console.log(person);

  return <div>1</div>;
}


7. Exclude<T, U>

  • 作用:T中取T、U交集的补集 Exclude 就是从一个联合类型中排除掉属于另一个联合类型的子集,下面是其声明的形式:
/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

使用示例如下:

export default function App() {
  type T0 = Exclude<"a" | "b" | "c", "a">;      
  // "b" | "c"
  type T1 = Exclude<"a" | "b" | "c", "a" | "b">;     
  // "c"
  type T2 = Exclude<string | number | (() => void), Function>; 
  // string | number

  const person:T0 ='b' 
  
  console.log(person);

  return <div>1</div>;
}

8. Pick<T, K extends keyof T>

从T类型中挑选部分属性K来构造新的类型(与omit相反,omit是剔除声明项,pick是挑选声明项),它的声明形式如下:

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

使用示例如下:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  const person1: Pick<IPerson, "name" | "age"> = {
  //挑选出自己需要的选项
    name: "zgc",
    age: 1,
  };
  //不能有height属性
  
  const person2: IPerson = {
    name: "zgc",
    age: 1,
    height: 1,
  };

  console.log(person1, person2);

  return <div>1</div>;
}

9. Omit<T, K extends keyof any>

上面的Pick 和 Exclude 都是最基础基础的工具泛型,很多时候用 Pick 或者 Exclude 还不如直接写类型更直接。而 Omit 就基于这两个来做的一个更抽象的封装,它允许从一个对象中剔除若干个属性,剩下的就是需要的新类型。下面是它的声明形式:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

使用示例如下:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  const person1: Omit<IPerson, "age" | "height"> = {
    //剔除自己不需要的选项
    name: "zgc",
  };

  const person2: IPerson = {
    name: "zgc",
    age: 1,
    height: 1,
  };

  console.log(person1, person2);

  return <div>1</div>;
}

Omit、Pick、Exclude的相同点与不同点:

  • 相同点:

    • 三种工具类都是使用已有的类型进行属性过滤获得新的类型
    • Omit和Exclude都是做属性剔除
    • Omit和Pick的参数类型都是<{key:value},key>
  • 不同点:

    • Exclude的参数类型没有限制,可以是字面量也可以是具体的类型如string、boolean等,而Omit和Pick的第二参数则必须是第一参数的子属性
  • Pick搭配Exclude实现Omit:Exclude先剔除不要的键名,挑出想要的键名,Pick再从键值对中根据键名挑选出来。
    实现公式:Omit=Pick<T,Exclude<keyof T,K>>。其中:T:<key,value> K:key

10. Record<K extends keyof any, T>

Record 用来构造一个类型,其属性名的类型为K,属性值的类型为T这个工具泛型可用来将某个类型的属性映射到另一个类型上,下面是其声明形式:

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

使用示例如下:

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  type IPage = "home" | "about" | "contact";
  //const page: Record<K, T> = {
  //const page: Record<属性名, 属性值> = {
  //其中k需要满足约束“string | number | symbol”。
  const page: Record<IPage, IPerson> = {
    about: { name: "zgc", age: 1, height: 1 },
    contact: { name: "wf", age: 1, height: 1 },
    home: { name: "wlc", age: 1, height: 1 },
  };
  console.log(page);
  return <div>1</div>;
}

1639708625(1).png
1639708625(1).png

11. ReturnType

ReturnType会返回函数返回值的类型,其声明形式如下:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

使用示例如下:

import React from "react";

export default function App() {
  function foo(type: number): boolean {
    return type === 0;
  }

  type FooType = ReturnType<typeof foo>;
  const a: FooType = true;
  console.log(a);

  return <div>1</div>;
}

这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) => boolean。

  • 类型可以索引返回子属性类型
function foo() {
  return {
    a: 1,
    b: 2,
    subInstArr: [1, 2],
  };
}

type InstType = ReturnType<typeof foo>;
type SubInstArr = InstType["subInstArr"];
type SubIsntType = SubInstArr[0];

const baz: SubIsntType = 3;

// 也可一步到位
type SubIsntType2 = ReturnType<typeof foo>["subInstArr"][0];
const baz2: SubIsntType2 = 1;
console.log(baz, baz2);

function Counter() {
  return <></>;
}

export default Counter;

12. NonNullable

过滤掉 Type 中的 null 和 undefined,剩余的类型作为一个新类型返回。其实就是 Exclude 的一种特殊情况。

源码解析:

type NonNullable<T> = T extends null | undefined ? never : T

可以发现和 Exclude<T, U> 的源码非常像,只是把 U 换成了 null | undefined 。所以结合Exclude<T, U> 还是很好理解的。

实战用法:

type T0 = NonNullable<string | number | undefined>;

//  type T0 = string | number

type T1 = NonNullable<string[] | null | undefined>;

//  type T1 = string[]

七、Axios 封装

在React项目中,我们经常使用Axios库进行数据请求,Axios 是基于 Promise 的 HTTP 库,可以在浏览器和 node.js 中使用。Axios 具备以下特性:

  • 从浏览器中创建 XMLHttpRequests;
  • 从 node.js 创建 HTTP 请求;
  • 支持 Promise API;
  • 拦截请求和响应;
  • 转换请求数据和响应数据;
  • 取消请求;
  • 自动转换 JSON 数据;
  • 客户端支持防御 XSRF。

Axios的基本使用就不再多介绍了。为了更好地调用,做一些全局的拦截,通常会对Axios进行封装,下面就使用TypeScript对Axios进行简单封装,使其同时能够有很好的类型支持。Axios是自带声明文件的,所以我们无需额外的操作。

下面来看基本的封装:

import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise,AxiosResponse } from 'axios'; // 引入axios和定义在node_modules/axios/index.ts文件里的类型声明

 // 定义接口请求类,用于创建axios请求实例
class HttpRequest {
  // 接收接口请求的基本路径
  constructor(public baseUrl: string) { 
    this.baseUrl = baseUrl;
  }
  
  // 调用接口时调用实例的这个方法,返回AxiosPromise
  public request(options: AxiosRequestConfig): AxiosPromise { 
    // 创建axios实例,它是函数,同时这个函数包含多个属性
    const instance: AxiosInstance = axios.create() 
    // 合并基础路径和每个接口单独传入的配置,比如url、参数等
    options = this.mergeConfig(options) 
    // 调用interceptors方法使拦截器生效
    this.interceptors(instance, options.url) 
    // 返回AxiosPromise
    return instance(options) 
  }
  
  // 用于添加全局请求和响应拦截
  private interceptors(instance: AxiosInstance, url?: string) { 
    // 请求和响应拦截
  }
  
  // 用于合并基础路径配置和接口单独配置
  private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig { 
    return Object.assign({ baseURL: this.baseUrl }, options);
  }
}
export default HttpRequest;

通常baseUrl在开发环境的和生产环境的路径是不一样的,所以可以根据当前是开发环境还是生产环境做判断,应用不同的基础路径。这里要写在一个配置文件里:

export default {
    api: {
        devApiBaseUrl: '/test/api/xxx',
        proApiBaseUrl: '/api/xxx',
    },
};

在上面的文件中引入这个配置:

import { api: { devApiBaseUrl, proApiBaseUrl } } from '@/config';
const apiBaseUrl = env.NODE_ENV === 'production' ? proApiBaseUrl : devApiBaseUrl;

之后就可以将apiBaseUrl作为默认值传入HttpRequest的参数:

class HttpRequest { 
  constructor(public baseUrl: string = apiBaseUrl) { 
    this.baseUrl = baseUrl;
  }

接下来可以完善一下拦截器类,在类中interceptors方法内添加请求拦截器和响应拦截器,实现对所有接口请求的统一处理:

private interceptors(instance: AxiosInstance, url?: string) {
  	// 请求拦截
    instance.interceptors.request.use((config: AxiosRequestConfig) => {
      // 接口请求的所有配置,可以在axios.defaults修改配置
      return config
    },
    (error) => {
      return Promise.reject(error)
    })
 	
  	// 响应拦截
    instance.interceptors.response.use((res: AxiosResponse) => {
      const { data } = res 
      const { code, msg } = data
      if (code !== 0) {
        console.error(msg) 
      }
      return res
    },
    (error) => { 
      return Promise.reject(error)
    })
  }

到这里封装的就差不多了,一般服务端会将状态码、提示信息和数据封装在一起,然后作为数据返回,所以所有请求返回的数据格式都是一样的,所以就可以定义一个接口来指定返回的数据结构,可以定义一个接口:

export interface ResponseData {
  code: number
  data?: any
  msg: string
}

接下来看看使用TypeScript封装的Axios该如何使用。可以先定义一个请求实例:

import HttpRequest from '@/utils/axios'
export * from '@/utils/axios'
export default new HttpRequest()

这里把请求类导入进来,默认导出这个类的实例。之后创建一个登陆接口请求方法:

import axios, { ResponseData } from './index'
import { AxiosPromise } from 'axios'

interface ILogin {
  user: string;
  password: number | string
}

export const loginReq = (data: ILogin): AxiosPromise<ResponseData> => {
  return axios.request({
    url: '/api/user/login',
    data,
    method: 'POST'
  })
}

这里封装登录请求方法loginReq,他的参数必须是我们定义的ILogin接口的类型。这个方法返回一个类型为AxiosPromise的Promise,AxiosPromise是axios声明文件内置的类型,可以传入一个泛型变量参数,用于指定返回的结果中data字段的类型。

接下来可以调用一下这个登录的接口:

import { loginReq } from '@/api/user'

const Home: FC = () => {
  const login = (params) => {
  	loginReq(params).then((res) => {
    	console.log(res.data.code)
  	})	
  }  
}

通过这种方式,当我们调用loginReq接口时,就会提示我们,参数的类型是ILogin,需要传入几个参数。这样编写代码的体验就会好很多。

八. 其他

1. import React

在React项目中使用TypeScript时,普通组件文件后缀为.tsx,公共方法文件后缀为.ts。在. tsx 文件中导入 React 的方式如下:

import * as React from 'react'
import * as ReactDOM from 'react-dom'

这是一种面向未来的导入方式,如果想在项目中使用以下导入方式:

import React from "react";
import ReactDOM from "react-dom";

就需要在tsconfig.json配置文件中进行如下配置:

"compilerOptions": {
    // 允许默认从没有默认导出的模块导入。
    "allowSyntheticDefaultImports": true,
}

2. Types or Interfaces?

我们可以使用types或者Interfaces来定义类型吗,那么该如何选择他俩呢?建议如下:

  • 在定义公共 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对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而interface更适合声明字典类行,然后定义或者扩展它。

3. 懒加载类型

如果我们想在React router中使用懒加载,React也为我们提供了懒加载方法的类型,来看下面的例子:

export interface RouteType {
    pathname: string;
    component: LazyExoticComponent<any>;
    exact: boolean;
    title?: string;
    icon?: string;
    children?: RouteType[];
}
export const AppRoutes: RouteType[] = [
    {
        pathname: '/login',
        component: lazy(() => import('../views/Login/Login')),
        exact: true
    },
    {
        pathname: '/404',
        component: lazy(() => import('../views/404/404')),
        exact: true,
    },
    {
        pathname: '/',
        exact: false,
        component: lazy(() => import('../views/Admin/Admin'))
    }
]

下面是懒加载类型和lazy方法在声明文件中的定义:

type LazyExoticComponent<T extends ComponentType<any>> = ExoticComponent<ComponentPropsWithRef<T>> & {
  readonly _result: T;
};

function lazy<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;

4. 类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。在React项目中,断言还是很有用的,有时候推断出来的类型并不是真正的类型,很多时候我们可能会比TS更懂我们的代码,所以可以使用断言(使用as关键字)来定义一个值得类型。

来看下面的例子:

const getLength = (target: string | number): number => {
  if (target.length) { // error 类型"string | number"上不存在属性"length"
    return target.length; // error  类型"number"上不存在属性"length"
  } else {
    return target.toString().length;
  }
};

当TypeScript不确定一个联合类型的变量到底是哪个类型时,就只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数target和返回值的类型定义之后就会报错。这时就可以使用断言,将target的类型断言成string类型:

const getStrLength = (target: string | number): number => {
  if ((target as string).length) {      
    return (target as string).length; 
  } else {
    return target.toString().length;
  }
};

需要注意,类型断言并不是类型转换,断言成一个联合类型中不存在的类型是不允许的。

再来看一个例子,在调用一个方法时传入参数: image.png 这里就提示我们这个参数可能是undefined,而通过业务知道这个值是一定存在的,所以就可以将它断言成数字:data?.subjectId as number

除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。

感悟:使用类型断言真的能解决项目中的很多报错~

5. 枚举类型

枚举类型在项目中的作用也是不可忽视的,使用枚举类型可以让代码的扩展性更好,当我想更改某属性值时,无需去全局更改这个属性,只要更改枚举中的值即可。通常情况下,最好新建一个文件专门来定义枚举值,便于引用

TypeScript 在 ES 原有类型基础上加入枚举类型,使得在 TypeScript 中也可以给一组数值赋予名字,这样对开发者比较友好,可以理解枚举就是一个字典。枚举类型使用enum来定义:

enum Day {
  SUNDAY,
  MONDAY,
  TUESDAY,
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY
 }

上面定义的枚举类型的Day,它有7个值,TypeScript会为它们每个值分配编号,默认从0开始,在使用时,就可以使用名字而不需要记数字和名称的对应关系了:

enum Day {
  SUNDAY = 0,
  MONDAY = 1,
  TUESDAY = 2,
  WEDNESDAY = 3,
  THURSDAY = 4,
  FRIDAY = 5,
  SATURDAY = 6
}

下面是将上面代码转译为 JavaScript 后的效果:

var Day = void 0;
(function (Day) {
  Day[Day["SUNDAY"] = 0] = "SUNDAY";
  Day[Day["MONDAY"] = 1] = "MONDAY";
  Day[Day["TUESDAY"] = 2] = "TUESDAY";
  Day[Day["WEDNESDAY"] = 3] = "WEDNESDAY";
  Day[Day["THURSDAY"] = 4] = "THURSDAY";
  Day[Day["FRIDAY"] = 5] = "FRIDAY";
  Day[Day["SATURDAY"] = 6] = "SATURDAY";
})(Day || (Day = {}));

可以看到,每一个值都被赋予了对应的数字。

在TypeScript中,我们需要通过点的形式获取枚举集合中的成员:

console.log(Day.SUNDAY)   // 0
console.log(Day.MONDAY)   // 1

说完枚举类型的基本使用,下面就来看一下常见的枚举类型。

1. 数字枚举

在上面的例子中,在仅指定常量命名的情况下,定义的就是一个默认从 0 开始递增的数字集合,称之为数字枚举。如果想要从其他值开始递增,可以将第一个值的索引值进行指定:

enum Color {
  Red = 2,
  Blue,
  Yellow
}
console.log(Color.Red, Color.Blue, Color.Yellow); // 2 3 4

可以对一个字段指定一个索引值,那他后面没有指定索引值的就会依次加一:

// 指定部分字段,其他使用默认递增索引
enum Status {
  Ok = 200,
  Created,
  Accepted,
  BadRequest = 400,
  Unauthorized
}
console.log(Status.Created, Status.Accepted, Status.Unauthorized); // 201 202 401

除此之外,还可以给每个字段指定不连续的任意索引值:

enum Status {
  Success = 200,
  NotFound = 404,
  Error = 500
}
console.log(Status.Success, Status.NotFound, Status.Error); // 200 404 500

数字枚举在定义值时,可以使用计算值和常量。但是要注意,如果某个字段使用了计算值或常量,那么该字段后面紧接着的字段必须设置初始值,这里不能使用默认的递增值了,来看例子:

// 初值为计算值
const getValue = () => {
  return 0;
};
enum ErrorIndex {
  a = getValue(),
  b, // error 枚举成员必须具有初始化的值
  c
}
enum RightIndex {
  a = getValue(),
  b = 1,
  c
}
// 初值为常量
const Start = 1;
enum Index {
  a = Start,
  b, // error 枚举成员必须具有初始化的值
  c
}

2. 字符串枚举

TypeScript 将定义值是字符串字面量的枚举称为字符串枚举,字符串枚举值要求每个字段的值都必须是字符串字面量,或者是该枚举值中另一个字符串枚举成员

// 使用字符串字面量
enum Message {
  Error = "Sorry, error",
  Success = "Hoho, success"
}
console.log(Message.Error); // 'Sorry, error'

// 使用枚举值中其他枚举成员
enum Message {
  Error = "error message",
  ServerError = Error,
  ClientError = Error
}
console.log(Message.Error); // 'error message'
console.log(Message.ServerError); // 'error message'

注意,这里的其他枚举成员指的是同一个枚举值中的枚举成员,因为字符串枚举不能使用常量或者计算值,所以不能使用其他枚举值中的成员。

3. 反向映射

定义枚举类型的值时,可以通过 Enum['key'] 或者 Enum.key 的形式获取到对应的值 value。TypeScript 还支持反向映射,但是反向映射只支持数字枚举,不支持字符串枚举。来看下面的例子:

enum Status {
  Success = 200,
  NotFound = 404,
  Error = 500
}
console.log(Status["Success"]); // 200
console.log(Status[200]); // 'Success'
console.log(Status[Status["Success"]]); // 'Success'

TypeScript 中定义的枚举,编译之后其实是一个对象,生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。下面来看看上面代码中的 Status 编译后的效果:

{
    200: "Success",
    404: "NotFound",
    500: "Error",
    Error: 500,
    NotFound: 404,
    Success: 200
}

可以看到,TypeScript 会把定义的枚举值的字段名分别作为对象的属性名和属性值,把枚举值的字段值分别作为对象的属性值和属性名,同时添加到对象中。这样既可以通过枚举值的字段名得到值,也可以通过枚举值的值得到字段名。

4. 异构枚举

异构枚举就是枚举值中成员值既有数字类型又有字符串类型,如下:

enum Result {
  Faild = 0,
  Success = "Success"
}

在开发过程中不建议使用异步枚举。因为往往将一类值整理为一个枚举值时,它们的特点是相似的。比如在做接口请求时的返回状态码,如果是状态码都是数值,如果是提示信息,都是字符串,所以在使用枚举的时候,往往是可以避免使用异构枚举的,主要是做好类型的整理。

5. 常量枚举

在TypeScript中,定义了枚举值之后,编译成 JavaScript 的代码会创建一个对应的对象,这个对象可以在程序运行时使用。但是如果使用枚举只是为了让程序可读性好,并不需要编译后的对象呢?这样会增加一些编译后的代码量。TypeScript 中有一个const enum(常量枚举),在定义枚举的语句之前加上const关键字,这样编译后的代码不会创建这个对象,只是会从枚举里拿到相应的值进行替换:

enum Status {
  Off,
  On
}
const enum Animal {
  Dog,
  Cat
}
const status = Status.On;
const animal = Animal.Dog;

上面的代码编译成 JavaScript 之后是这样的:

var Status;
(function(Status) {
  Status[(Status["Off"] = 0)] = "Off";
  Status[(Status["On"] = 1)] = "On";
})(Status || (Status = {}));
var status = Status.On;
var animal = 0; // Dog 

对于 Status 的处理,先是定义一个变量 Status,然后定义一个立即执行函数,在函数内给 Status 添加对应属性,首先Status[“Off”] = 0是给Status对象设置Off属性,并且值设为 0,这个赋值表达式的返回值是等号右边的值,也就是 0,所以Status[Status[“Off”] = 0] = "Off"相当于Status[0] = “Off”。创建了这个对象之后,将 Status 的 On 属性值赋值给 status;再来看下 animal 的处理,编译后的代码并没有像Status创建一个Animal对象,而是直接把Animal.Dog的值0替换到了const animal = Animal.Dog表达式的Animal.Dog位置。

通过定义常量枚举,可以以清晰、结构化的形式维护相关联的常量集合。而且因为转译后抹除了定义、内联成员值,所以在代码的体积和性能方面并不会比直接内联常量值差。

6. 枚举成员类型和联合枚举类型

如果枚举值里所有成员都是字面量类型的值,那么枚举的每个成员和枚举值本身都可以作为类型来使用,我们称这样的枚举成员为字面量枚举成员。满足条件的枚举成员的值有以下三种:

  • 没有初始值的枚举成员,例如:enum E { A }
  • 值为字符串字面量,例如:enum E { A = 'a' }
  • 值为数值字面量,或者带有-符号的数值字面量,例如:enum E { A = 1 }enum E { A = -1 }
(1)枚举成员类型

当所有枚举成员都拥有字面量枚举值时,就枚举成员成为了类型:

enum Animal {
  Dog = 1,
  Cat = 2
}

interface Dog {
  type: Animal.Dog; 
}
interface Cat {
  type: Animal.Cat; 
}

let cat: Cat = {
  type: Animal.Dog // error [ts] 不能将类型“Animal.Dog”分配给类型“Animal.Cat”
};
let dog: Dog = {
  type: Animal.Dog
};

可以看到,代码的第七行使用Animal.Dog作为类型,指定接口Dog的必须有一个type字段,且类型为Animal.Dog。

(2)联合枚举类型

当枚举值符合条件时,这个枚举值就可以看做是一个包含所有成员的联合类型:

enum Status {
  Off,
  On
}
interface Light {
  status: Status;
}
enum Animal {
  Dog = 1,
  Cat = 2
}
const light1: Light = {
  status: Animal.Dog // error 不能将类型“Animal.Dog”分配给类型“Status”
};
const light2: Light = {
  status: Status.Off
};
const light3: Light = {
  status: Status.On
};

上面例子定义接口 Light 的 status 字段的类型为枚举值 Status,那么此时 status 的属性值必须为 Status.Off 和 Status.On 中的一个,也就是相当于status: Status.Off | Status.On

7. 枚举合并

说完常见的枚举类型,最后来看看枚举合并的概念。对于枚举类型的值,我们可以分开进行声明:

enum Day {
  SUNDAY,
  MONDAY,
  TUESDAY
 }

enum Day {
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY
 }

这时 TypeScript 就会对这个枚举值进行合并操作,合并后编译为JavaScript的代码如下:

var Day = void 0;
(function (Day) {
  Day[Day["SUNDAY"] = 0] = "SUNDAY";
  Day[Day["MONDAY"] = 1] = "MONDAY";
  Day[Day["TUESDAY"] = 2] = "TUESDAY";
  Day[Day["WEDNESDAY"] = 3] = "WEDNESDAY";
  Day[Day["THURSDAY"] = 4] = "THURSDAY";
  Day[Day["FRIDAY"] = 5] = "FRIDAY";
  Day[Day["SATURDAY"] = 6] = "SATURDAY";
})(Day || (Day = {}));

8. 应用: 消除魔术数字/字符

本人比较痛恨的一些代码点。

  • 糟糕的例子,看到下面这段代码不知道你的内心,有没有羊驼奔腾。
if (status === 0) {
    // ...
} else {
    // ...
}

// ...

if (status === 1) {
    // ...
}

  • 利用枚举,统一注释且语义化
// enum.ts
export enum StatusEnum {
    Doing,   // 进行中
    Success, // 成功
    Fail,    // 失败
}

//index.tsx
if (status === StatusEnum.Doing) {
    // ...
} else {
    // ...
}

// ...

if (status === StatusEnum.Success) {
    // ...
}

9. 延伸:策略模式消除 if、else

// 对象常量
export const StatusEnum = {
    Doing: 0,   // 进行中
    Success: 1, // 成功
    Fail: 2,    // 失败
};


if (status === StatusEnum.Doing) {
    return '进行中';
} else if (status === StatusEnum.Success) {
    return '成功';
} else {
    return '失败';
}

  • 策略模式
// 对象常量
export const StatusEnumText = {
    [StatusEnum.Doing]: '进行中',
    [StatusEnum.Success]: '成功',
    [StatusEnum.Fail]: '失败',
};

// ...
return StatusEnumText[status];

6. Promise 类型

在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。

Promise<T> 是一个泛型类型,T 泛型变量用于确定使用 then 方法时接收的第一个回调函数(onfulfilled)的参数类型。

实例


interface 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}

Promise<T> 实现源码 node_modules/typescript/lib/lib.es5.d.ts

interface Promise<T> {
    /**
     * Attaches callbacks for the resolution and/or rejection of the Promise.
     * @param onfulfilled The callback to execute when the Promise is resolved.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of which ever callback is executed.
     */
    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
    /**
     * Attaches a callback for only the rejection of the Promise.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of the callback.
     */
    catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

7. forwardRef

函数式组件默认不可以加 ref,它不像类组件那样有自己的实例。forwardRef 一般是函数式组件用来接收父组件传来的 ref。

所以需要标注好实例类型,也就是父组件通过 ref 可以拿到什么样类型的值。

子组件

export type Props = { };
export type Ref = HTMLButtonElement;
export const FancyButton = React.forwardRef<Ref, Props>((props, ref) => (
  <button ref={ref} className="MyClassName">
    {props.children}
  </button>
));

由于这个例子里直接把 ref 转发给 button 了,所以直接把类型标注为 HTMLButtonElement 即可。

父组件这样调用,就可以拿到正确类型:

export const App = () => {
  const ref = useRef<HTMLButtonElement>()
  return (
    <FancyButton ref={ref} />
  )
}

8. 获取未导出的Type

某些场景下我们在引入第三方的库时会发现想要使用的组件并没有导出我们需要的组件参数类型,这时候我们可以通过 ComponentProps来获取到想要的类型。

// 获取参数类型

import { Button } from 'library' // 但是未导出props 类型

type ButtonProps = React.ComponentProps<typeof Button> // 获取props类型

//然后可以操作使用了

type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick

const AlertButton: React.FC<AlertButtonProps> = props => (
  <Button onClick={() => alert('hello')} {...props} />
)

参考文章链接

本文转自 https://juejin.cn/post/7041890221658472461open in new window,如有侵权,请联系删除。

上次编辑于:
本站勉强运行 小时
本站总访问量
網站計數器