如何優雅地在 React 中使用 TypeScript,看這一篇就夠了!

畢業已有 3 月有餘,工作用的技術棧主要是 React hooks + TypeScript。其實在單獨使用 TypeScript 時沒有太多的坑,不過和 React 結合之後就會複雜很多。本文就來聊一聊 TypeScript 與 React 一起使用時經常遇到的一些類型定義的問題。閱讀本文前,希望你能有一定的 React 和 TypeScript 基礎。

一、組件聲明

在 React 中,組件的聲明方式有兩種:函數組件類組件, 來看看這兩種類型的組件聲明時是如何定義 TS 類型的。

1. 類組件

類組件的定義形式有兩種:React.Component<P, S={}>React.PureComponent<P, S={} SS={}>,它們都是泛型接口,接收兩個參數,第一個是 props 類型的定義,第二個是 state 類型的定義,這兩個參數都不是必須的,沒有時可以省略:

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;

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 的類型,只有在調用時才知道組件類型,該怎麼辦呢?這時泛型就發揮作用了:

// 定義組件
class MyComponent<P> extends React.Component<P> {
  internalProp: P;
  constructor(props: P) {
    super(props);
    this.internalProp = props;
  }
  render() {
    return (
      <span>hello world</span>
    );
  }
}

// 使用組件
type IProps = { name: string; age: number; };

<MyComponent<IProps>  age={18} />;          // Success
<MyComponent<IProps>  />;  // Error

2. 函數組件

通常情況下,函數組件我是這樣寫的:

interface IProps {
  name: string
}

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

  return (
    <div class>
      <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>

最終的定義形式如下:

interface IProps {
  name: string
}

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

export default App;

當使用這種形式來定義函數組件時,props 中默認會帶有 children 屬性,它表示該組件在調用時,其內部的元素,來看一個例子,首先定義一個組件,組件中引入了 Child1 和 Child2 組件:

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 組件結構如下:

interface IProps {
  name: string;
}
const Child1: React.FC<IProps> = (props) ={
  const { name, children } = props;
  console.log(children);
  return (
    <div class>
      <h1>hello child1</h1>
      <h2>{name}</h2>
    </div>
  );
};

export default Child1;

我們在 Child1 組件中打印了 children 屬性,它的值是一個數組,包含 Child2 對象和後面的文本:

使用 React.FC 聲明函數組件和普通聲明的區別如下:

那如果我們在定義組件時不知道 props 的類型,只有調用時才知道,那就還是用泛型來定義 props 的類型。對於使用 function 定義的函數組件:

// 定義組件
function MyComponent<P>(props: P) {
  return (
   <span>
     {props}
    </span>
  );
}

// 使用組件
type IProps = { name: string; age: number; };

<MyComponent<IProps>  age={18} />;          // Success
<MyComponent<IProps>  />;  // Error

如果使用箭頭函數定義的函數組件,直接這樣調用是錯誤的:

const MyComponent = <P>(props: P) {
  return (
   <span>
     {props}
    </span>
  );
}

必須使用 extends 關鍵字來定義泛型參數才能被成功解析:

const MyComponent = <P extends any>(props: P) {
  return (
   <span>
     {props}
    </span>
  );
}

二、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. CSSProperties

先來看看 React 的聲明文件中對 CSSProperties 的定義:

export interface CSSProperties extends CSS.Properties<string | number> {
  /**
   * The index signature was removed to enable closed typing for style
   * using CSSType. You're able to use type assertion or module augmentation
   * to add properties or an index signature of your own.
   *
   * For examples and more information, visit:
   * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
   */
}

React.CSSProperties 是 React 基於 TypeScript 定義的 CSS 屬性類型,可以將一個方法的返回值設置爲該類型:

import * as React from "react";

const classNames = require("./sidebar.css");

interface Props {
  isVisible: boolean;
}

const divStyle = (props: Props): React.CSSProperties =({
  width: props.isVisible ? "23rem" : "0rem"
});

export const SidebarComponent: React.StatelessComponent<Props> = props =(
  <div id="mySidenav" className={classNames.sidenav} style={divStyle(props)}>
    {props.children}
  </div>
);

這裏 divStyle 組件的返回值就是 React.CSSProperties 類型。

我們還可以定義一個 CSSProperties 類型的變量:

const divStyle: React.CSSProperties = {
    width: "11rem",
    height: "7rem",
    backgroundColor: `rgb(${props.color.red},${props.color.green}${props.color.blue})`
};

這個變量可以在 HTML 標籤的 style 屬性上使用:

<div style={divStyle} />

在 React 的類型聲明文件中,style 屬性的類型如下:

style?: CSSProperties | undefined;

三、React Hooks

1. useState

默認情況下,React 會爲根據設置的 state 的初始值來自動推導 state 以及更新函數的類型:

如果已知 state 的類型,可以通過以下形式來自定義 state 的類型:

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

如果初始值爲 null,需要顯式地聲明 state 的類型:

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

如果 state 是一個對象,想要初始化一個空對象,可以使用斷言來處理:

const [user, setUser] = React.useState<IUser>({} as IUser);

實際上,這裏將空對象 {} 斷言爲 IUser 接口就是欺騙了 TypeScript 的編譯器,由於後面的代碼可能會依賴這個對象,所以應該在使用前及時初始化 user 的值,否則就會報錯。

下面是聲明文件中 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>()[| 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 的主要作用就是處理副作用,它的第一個參數是一個函數,表示要清除副作用的操作,第二個參數是一組值,當這組值改變時,第一個參數的函數纔會執行,這讓我們可以控制何時運行函數來處理副作用:

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

當函數的返回值不是函數或者 effect 函數中未定義的內容時,如下:

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

TypeScript 就會報錯:

來看看 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 的第一個參數只允許返回一個函數。

3. useRef

當使用 useRef 時,我們可以訪問一個可變的引用對象。可以將初始值傳遞給 useRef,它用於初始化可變 ref 對象公開的當前屬性。當我們使用 useRef 時,需要給其指定類型:

const nameInput = React.useRef<HTMLInputElement>(null)

這裏給實例的類型指定爲了 input 輸入框類型。

當 useRef 的初始值爲 null 時,有兩種創建的形式,第一種:

const nameInput = React.useRef<HTMLInputElement>(null)
nameInput.current.innerText = "hello world";

這種形式下,ref1.current 是隻讀的(read-only),所以當我們將它的 innerText 屬性重新賦值時會報以下錯誤:

Cannot assign to 'current' because it is a read-only property.

那該怎麼將 current 屬性變爲動態可變的,先來看看類型聲明文件中 useRef 是如何定義的:

 function useRef<T>(initialValue: T): MutableRefObject<T>;
 // convenience overload for refs given as a ref prop as they typically start with a null value
 /**
   * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
   * (`initialValue`). The returned object will persist for the full lifetime of the component.
   *
   * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
   * value around similar to how you’d use instance fields in classes.
   *
   * Usage note: if you need the result of useRef to be directly mutable, include `| null` in the type
   * of the generic argument.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#useref
   */

這段代碼的第十行的告訴我們,如果需要 useRef 的直接可變,就需要在泛型參數中包含'| null',所以這就是當初始值爲 null 的第二種定義形式:

const nameInput = React.useRef<HTMLInputElement | null>(null);

這種形式下,nameInput.current 就是可寫的。不過兩種類型在使用時都需要做類型檢查:

nameInput.current?.innerText = "hello world";

那麼問題來了,爲什麼第一種寫法在沒有操作 current 時沒有報錯呢?因爲 useRef 在類型定義時具有多個重載聲明,第一種方式就是執行的以下函數重載:

function useRef<T>(initialValue: T|null): RefObject<T>;
// convenience overload for potentially undefined initialValue / call with 0 arguments
// has a default to stop it from defaulting to {} instead
/**
  * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
  * (`initialValue`). The returned object will persist for the full lifetime of the component.
  *
  * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
  * value around similar to how you’d use instance fields in classes.
  *
  * @version 16.8.0
  * @see https://reactjs.org/docs/hooks-reference.html#useref
  */

從上 useRef 的聲明中可以看到,function useRef 的返回值類型化是 MutableRefObject,這裏面的 T 就是參數的類型 T,所以最終 nameInput 的類型就是 React.MutableRefObject。

注意,上面用到了 HTMLInputElement 類型,這是一個標籤類型,這個操作就是用來訪問 DOM 元素的。

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 接收一個回調函數和一個依賴數組,只有當依賴數組中的值發生變化時纔會重新執行回調函數。來看一個例子:

const add = (a: number, b: number) => a + b;

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

這裏我們沒有給回調函數中的參數 a 定義類型,所以下面的調用方式都不會報錯:

memoizedCallback("hello");
memoizedCallback(5)

儘管 add 方法的兩個參數都是 number 類型,但是上述調用都能夠用執行。所以爲了更加嚴謹,我們需要給回調函數定義具體的類型:

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

這時候如果再給回調函數傳入字符串就會報錯了:

所有,需要注意,在使用 useCallback 時需要給回調函數的參數指定類型。

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 時需要定義返回值的類型:

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

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

如果返回值不一致,就會報錯:

const calculatedValue = useMemo<number>(() => a + "hello"[a]);
// 類型“() => string”的參數不能賦給類型“() => number”的參數

6. useContext

useContext 需要提供一個上下文對象,並返回所提供的上下文的值,當提供者更新上下文對象時,引用這些上下文對象的組件就會重新渲染:

const ColorContext = React.createContext({ color: "green" });

const Welcome = () ={
  const { color } = useContext(ColorContext);
  return <div style={{ color }}>hello world</div>;
};

在使用 useContext 時,會自動推斷出提供的上下文對象的類型,所以並不需要我們手動設置 context 的類型。當前,我們也可以使用泛型來設置 context 的類型:

interface IColor {
 color: string;
}

const ColorContext = React.createContext<IColor>({ color: "green" });

下面是 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
  */

7. useReducer

有時我們需要處理一些複雜的狀態,並且可能取決於之前的狀態。這時候就可以使用 useReducer,它接收一個函數,這個函數會根據之前的狀態來計算一個新的 state。其語法如下:

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

來看下面的例子:

const reducer = (state, action) ={
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

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

當前的狀態是無法推斷出來的,可以給 reducer 函數添加類型,通過給 reducer 函數定義 state 和 action 來推斷 useReducer 的類型,下面來修改上面的例子:

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

type State = { count: number };

const initialState: State = {count: 0}
const reducer = (state: State, action: ActionType) ={
  // ...
}

這樣,在 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) ={
  // ...
}

其實 dispatch 方法也是有類型的:

可以看到,dispatch 的類型是:React.Dispatch,上面示例的完整代碼如下:

import React, { useReducer } from "react";

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

type State = { count: number };

const Counter: React.FC = () ={
  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;

四、事件處理

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 事件對象如下:

可以看到,這些 Event 事件對象的泛型中都會接收一個 Element 元素的類型,這個類型就是我們綁定這個事件的標籤元素的類型,標籤元素類型將在下面的第五部分介紹。

來看一個簡單的例子:

type State = {
  text: string;
};

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

  const onChange = (e: React.FormEvent<HTMLInputElement>)void ={
    setText(e.currentTarget.value);
  };
  
  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
    </div>
  );
}

這裏就給 onChange 方法的事件對象定義爲了 FormEvent 類型,並且作用的對象是一個 HTMLInputElement 類型的標籤(input 標籤)

可以來看下 MouseEvent 事件對象和 ChangeEvent 事件對象的類型聲明,其他事件對象的聲明形似也類似:

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

interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
  target: EventTarget & T;
}

在很多事件對象的聲明文件中都可以看到 EventTarget 的身影。這是因爲,DOM 的事件操作(監聽和觸發),都定義在 EventTarget 接口上。EventTarget 的類型聲明如下:

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

比如在 change 事件中,會使用的 e.target 來獲取當前的值,它的的類型就是 EventTarget。來看下面的例子:

<input
 onChange={e => onSourceChange(e)}
 placeholder="最多30個字"
/>

const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) ={
    if (e.target.value.length > 30) {
      message.error('請長度不能超過30個字,請重新輸入');
      return;
    }
    setSourceInput(e.target.value);
};

這裏定義了一個 input 輸入框,當觸發 onChange 事件時,會調用 onSourceChange 方法,該方法的參數 e 的類型就是:React.ChangeEvent,而 e.target 的類型就是 EventTarget:

再來看一個例子:

questionList.map(item =(
    <div
     key={item.id}
   role="button"
   onClick={e => handleChangeCurrent(item, e)}
    >
    // 組件內容...
    </div>
)

const handleChangeCurrent = (item: IData, e: React.MouseEvent<HTMLDivElement>) ={
    e.stopPropagation();
    setCurrent(item);
};

這點代碼中,點擊某個盒子,就將它設置爲當前的盒子,方便執行其他操作。當鼠標點擊盒子時,會觸發 handleChangeCurren 方法,該方法有兩個參數,第二個參數是 event 對象,在方法中執行了 e.stopPropagation(); 是爲了阻止冒泡事件,這裏的 stopPropagation() 實際上並不是鼠標事件 MouseEvent 的屬性,它是合成事件上的屬性,來看看聲明文件中的定義:

interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
  //...     
}

interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
  //...
}

interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}

interface BaseSyntheticEvent<E = object, C = any, T = any> {
  nativeEvent: E;
  currentTarget: C;
  target: T;
  bubbles: boolean;
  cancelable: boolean;
  defaultPrevented: boolean;
  eventPhase: number;
  isTrusted: boolean;
  preventDefault(): void;
  isDefaultPrevented(): boolean;
  stopPropagation(): void;
  isPropagationStopped(): boolean;
  persist(): void;
  timeStamp: number;
  type: string;
}

可以看到,這裏的 stopPropagation() 是一層層的繼承來的,最終來自於 BaseSyntheticEvent 合成事件類型。原生的事件集合 SyntheticEvent 就是繼承自合成時間類型。SyntheticEvent<T = Element, E = Event> 泛型接口接收當前的元素類型和事件類型,如果不介意這兩個參數的類型,完全可以這樣寫:

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

2. 事件處理函數類型

說完事件對象類型,再來看看事件處理函數的類型。React 也爲我們提供了貼心的提供了事件處理函數的類型聲明,來看看所有的事件處理函數的類型聲明:

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];

type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
// 剪切板事件處理函數
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
// 複合事件處理函數
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
// 拖拽事件處理函數
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
// 焦點事件處理函數
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
// 表單事件處理函數
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
// Change事件處理函數
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
// 鍵盤事件處理函數
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
// 鼠標事件處理函數
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
// 觸屏事件處理函數
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
// 指針事件處理函數
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
// 界面事件處理函數
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
// 滾輪事件處理函數
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
// 動畫事件處理函數
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
// 過渡事件處理函數
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;

這裏面的 T 的類型也都是 Element,指的是觸發該事件的 HTML 標籤元素的類型,下面第五部分會介紹。

EventHandler 會接收一個 E,它表示事件處理函數中 Event 對象的類型。bivarianceHack 是事件處理函數的類型定義,函數接收一個 Event 對象,並且其類型爲接收到的泛型變量 E 的類型, 返回值爲 void。

還看上面的那個例子:

type State = {
  text: string;
};

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

  const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) ={
    setText(e.currentTarget.value);
  };
  
  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
    </div>
  );
}

這裏給 onChange 方法定義了方法的類型,它是一個 ChangeEventHandler 的類型,並且作用的對象是一個 HTMLImnputElement 類型的標籤(input 標籤)。

五、HTML 標籤類型

1. 常見標籤類型

在項目的依賴文件中可以找到 HTML 標籤相關的類型聲明文件:

所有的 HTML 標籤的類型都被定義在 intrinsicElements 接口中,常見的標籤及其類型如下:

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

那什麼時候會使用到標籤類型呢,上面第四部分的 Event 事件類型和事件處理函數類型中都使用到了標籤的類型。上面的很多的類型都需要傳入一個 ELement 類型的泛型參數,這個泛型參數就是對應的標籤類型值,可以根據標籤來選擇對應的標籤類型。這些類型都繼承自 HTMLElement 類型,如果使用時對類型類型要求不高,可以直接寫 HTMLELement。比如下面的例子:

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

其實,在直接操作 DOM 時也會用到標籤類型,雖然我們現在通常會使用框架來開發,但是有時候也避免不了直接操作 DOM。比如我在工作中,項目中的某一部分組件是通過 npm 來引入的其他組的組件,而在很多時候,我有需要動態的去個性化這個組件的樣式,最直接的辦法就是通過原生 JavaScript 獲取到 DOM 元素,來進行樣式的修改,這時候就會用到標籤類型。

來看下面的例子:

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

這是我最近寫的一段代碼(略微刪改),在第一頁有個 add-ele 元素的時候就刪除它。這裏我們將 item.firstChild 斷言成了 HTMLDivElement 類型,如果不斷言,item.firstChild 的類型就是 ChildNode,而 ChildNode 類型中是不存在 classList 屬性的,所以就就會報錯,當我們把他斷言成 HTMLDivElement 類型時,就不會報錯了。很多時候,標籤類型可以和斷言(as)一起使用。

後面在 removeChild 時又使用了 as 斷言,爲什麼呢?item.firstChild 不是已經自動識別爲 ChildNode 類型了嗎?因爲 TS 會認爲,我們可能不能獲取到類名爲 paper 的元素,所以 item.firstChild 的類型就被推斷爲 ChildNode | null,我們有時候比 TS 更懂我們定義的元素,知道頁面一定存在 paper 元素,所以可以直接將 item.firstChild 斷言成 ChildNode 類型。

2. 標籤屬性類型

衆所周知,每個 HTML 標籤都有自己的屬性,比如 Input 框就有 value、width、placeholder、max-length 等屬性,下面是 Input 框的屬性類型定義:

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

  onChange?: ChangeEventHandler<T> | undefined;
}

如果我們需要直接操作 DOM,就可能會用到元素屬性類型,常見的元素屬性類型如下:

一般情況下,我們是很少需要在項目中顯式的去定義標籤屬性的類型。如果子級去封裝組件庫的話,這些屬性就能發揮它們的作用了。來看例子(來源於網絡,僅供學習):

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. Partial

Partial 作用是將傳入的屬性變爲可選項。適用於對類型結構不明確的情況。它使用了兩個關鍵字:keyof 和 in,先來看看他們都是什麼含義。keyof 可以用來取得接口的所有 key 值:

interface IPerson {
  name: string;
  age: number;
  height: number;
}
type T = keyof IPerson 
// T 類型爲: "name" | "age" | "number"

in 關鍵字可以遍歷枚舉類型,:

type Person = "name" | "age" | "number"
type Obj =  {
  [p in Keys]: any
} 
// Obj類型爲: { name: any, age: any, number: 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] 取得相應屬性的值。中間的? 就用來將屬性設置爲可選。

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Partial<IPerson> = {
  name: "zhangsan";
}

2. Required

Required 的作用是將傳入的屬性變爲必選項,和上面的工具泛型恰好相反,其聲明如下:

/**
 * Make all properties in T required
 * 將T中的所有屬性設置爲必選
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

可以看到,這裏使用 -? 將屬性設置爲必選,可以理解爲減去問號。使用形式和上面的 Partial 差不多:

interface IPerson {
  name?: string;
  age?: number;
  height?: number;
}

const person: Required<IPerson> = {
  name: "zhangsan";
  age: 18;
  height: 180;
}

3. Readonly

將 T 類型的所有屬性設置爲只讀(readonly),構造出來類型的屬性不能被再次賦值。Readonly 的聲明形式如下:

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

使用示例如下:

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

const person: Readonly<IPerson> = {
  name: "zhangsan",
  age: 18
}

person.age = 20;  //  Error: cannot reassign a readonly property

可以看到,通過 Readonly 將 IPerson 的屬性轉化成了只讀,不能再進行賦值操作。

4. Pick<T, K extends keyof T>

從 T 類型中挑選部分屬性 K 來構造新的類型。它的聲明形式如下:

/**
 * 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];
};

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Pick<IPerson, "name" | "age"= {
  name: "zhangsan",
  age: 18
}

5. 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;
};

使用示例如下:

interface IPageinfo {
    title: string;
}

type IPage = 'home' | 'about' | 'contact';

const page: Record<IPage, IPageinfo> = {
    about: {title: 'about'},
    contact: {title: 'contact'},
    home: {title: 'home'},
}

6. Exclude<T, U>

Exclude 就是從一個聯合類型中排除掉屬於另一個聯合類型的子集,下面是其聲明的形式:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Exclude<IPerson, "age" | "sex"= {
  name: "zhangsan";
  height: 180;
}

7. 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>>;

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Omit<IPerson, "age" | "height"= {
  name: "zhangsan";
}

8. 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;

使用示例如下:

function foo(type): boolean {
  return type === 0
}

type FooType = ReturnType<typeof foo>

這裏使用 typeof 是爲了獲取 foo 的函數簽名,等價於 (type: any) => boolean。

七、Axios 封裝

在 React 項目中,我們經常使用 Axios 庫進行數據請求,Axios 是基於 Promise 的 HTTP 庫,可以在瀏覽器和 node.js 中使用。Axios 具備以下特性:

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 來定義類型嗎,那麼該如何選擇他倆呢?建議如下:

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

需要注意,類型斷言並不是類型轉換,斷言成一個聯合類型中不存在的類型是不允許的。

再來看一個例子,在調用一個方法時傳入參數:這裏就提示我們這個參數可能是 undefined,而通過業務知道這個值是一定存在的,所以就可以將它斷言成數字:data?.subjectId as number

除此之外,上面所說的標籤類型、組件類型、時間類型都可以使用斷言來指定給一些數據,還是要根據實際的業務場景來使用。

感悟:使用類型斷言真的能解決項目中的很多報錯~

5. 枚舉類型

枚舉類型在項目中的作用也是不可忽視的,使用枚舉類型可以讓代碼的擴展性更好,當我想更改某屬性值時,無需去全局更改這個屬性,只要更改枚舉中的值即可。通常情況下,最好新建一個文件專門來定義枚舉值,便於引用。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/l4sV6nHT7tPsKpLx5bu-6w