如何優雅地在 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 聲明函數組件和普通聲明的區別如下:
-
React.FC 顯式地定義了返回類型,其他方式是隱式推導的;
-
React.FC 對靜態屬性:displayName、propTypes、defaultProps 提供了類型檢查和自動補全;
-
React.FC 爲 children 提供了隱式的類型(ReactElement | null)。
那如果我們在定義組件時不知道 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>(): [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 的主要作用就是處理副作用,它的第一個參數是一個函數,表示要清除副作用的操作,第二個參數是一組值,當這組值改變時,第一個參數的函數纔會執行,這讓我們可以控制何時運行函數來處理副作用:
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]
);
這時候如果再給回調函數傳入字符串就會報錯了:
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 事件對象如下:
-
剪切板事件對象:ClipboardEvent<T = Element>
-
拖拽事件對象:DragEvent<T = Element>
-
焦點事件對象:FocusEvent<T = Element>
-
表單事件對象:FormEvent<T = Element>
-
Change 事件對象:ChangeEvent<T = Element>
-
鍵盤事件對象:KeyboardEvent<T = Element>
-
鼠標事件對象:MouseEvent<T = Element, E = NativeMouseEvent>
-
觸摸事件對象:TouchEvent<T = Element>
-
滾輪事件對象:WheelEvent<T = Element>
-
動畫事件對象:AnimationEvent<T = Element>
-
過渡事件對象:TransitionEvent<T = Element>
可以看到,這些 Event 事件對象的泛型中都會接收一個 Element 元素的類型,這個類型就是我們綁定這個事件的標籤元素的類型,標籤元素類型將在下面的第五部分介紹。
來看一個簡單的例子:
type State = {
text: string;
};
const App: React.FC = () => {
const [text, setText] = useState<string>("")
const onChange = (e: React.FormEvent<HTMLInputElement>): void => {
setText(e.currentTarget.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
);
}
這裏就給 onChange 方法的事件對象定義爲了 FormEvent 類型,並且作用的對象是一個 HTMLInputElement 類型的標籤(input 標籤)
可以來看下 MouseEvent 事件對象和 ChangeEvent 事件對象的類型聲明,其他事件對象的聲明形似也類似:
interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
altKey: boolean;
button: number;
buttons: number;
clientX: number;
clientY: number;
ctrlKey: boolean;
/**
* See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
*/
getModifierState(key: string): boolean;
metaKey: boolean;
movementX: number;
movementY: number;
pageX: number;
pageY: number;
relatedTarget: EventTarget | null;
screenX: number;
screenY: number;
shiftKey: boolean;
}
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
target: EventTarget & T;
}
在很多事件對象的聲明文件中都可以看到 EventTarget 的身影。這是因爲,DOM 的事件操作(監聽和觸發),都定義在 EventTarget 接口上。EventTarget 的類型聲明如下:
interface EventTarget {
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
dispatchEvent(evt: Event): boolean;
removeEventListener(type: string, listener?: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}
比如在 change 事件中,會使用的 e.target 來獲取當前的值,它的的類型就是 EventTarget。來看下面的例子:
<input
onChange={e => onSourceChange(e)}
placeholder="最多30個字"
/>
const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length > 30) {
message.error('請長度不能超過30個字,請重新輸入');
return;
}
setSourceInput(e.target.value);
};
這裏定義了一個 input 輸入框,當觸發 onChange 事件時,會調用 onSourceChange 方法,該方法的參數 e 的類型就是:React.ChangeEvent,而 e.target 的類型就是 EventTarget:
再來看一個例子:
questionList.map(item => (
<div
key={item.id}
role="button"
onClick={e => handleChangeCurrent(item, e)}
>
// 組件內容...
</div>
)
const handleChangeCurrent = (item: IData, e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
setCurrent(item);
};
這點代碼中,點擊某個盒子,就將它設置爲當前的盒子,方便執行其他操作。當鼠標點擊盒子時,會觸發 handleChangeCurren 方法,該方法有兩個參數,第二個參數是 event 對象,在方法中執行了 e.stopPropagation(); 是爲了阻止冒泡事件,這裏的 stopPropagation() 實際上並不是鼠標事件 MouseEvent 的屬性,它是合成事件上的屬性,來看看聲明文件中的定義:
interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
//...
}
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
//...
}
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
interface BaseSyntheticEvent<E = object, C = any, T = any> {
nativeEvent: E;
currentTarget: C;
target: T;
bubbles: boolean;
cancelable: boolean;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
preventDefault(): void;
isDefaultPrevented(): boolean;
stopPropagation(): void;
isPropagationStopped(): boolean;
persist(): void;
timeStamp: number;
type: string;
}
可以看到,這裏的 stopPropagation() 是一層層的繼承來的,最終來自於 BaseSyntheticEvent 合成事件類型。原生的事件集合 SyntheticEvent 就是繼承自合成時間類型。SyntheticEvent<T = Element, E = Event> 泛型接口接收當前的元素類型和事件類型,如果不介意這兩個參數的類型,完全可以這樣寫:
<input
onChange={(e: SyntheticEvent<Element, Event>)=>{
//...
}}
/>
2. 事件處理函數類型
說完事件對象類型,再來看看事件處理函數的類型。React 也爲我們提供了貼心的提供了事件處理函數的類型聲明,來看看所有的事件處理函數的類型聲明:
type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];
type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
// 剪切板事件處理函數
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
// 複合事件處理函數
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
// 拖拽事件處理函數
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
// 焦點事件處理函數
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
// 表單事件處理函數
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
// Change事件處理函數
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
// 鍵盤事件處理函數
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
// 鼠標事件處理函數
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
// 觸屏事件處理函數
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
// 指針事件處理函數
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
// 界面事件處理函數
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
// 滾輪事件處理函數
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
// 動畫事件處理函數
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
// 過渡事件處理函數
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;
這裏面的 T 的類型也都是 Element,指的是觸發該事件的 HTML 標籤元素的類型,下面第五部分會介紹。
EventHandler 會接收一個 E,它表示事件處理函數中 Event 對象的類型。bivarianceHack 是事件處理函數的類型定義,函數接收一個 Event 對象,並且其類型爲接收到的泛型變量 E 的類型, 返回值爲 void。
還看上面的那個例子:
type State = {
text: string;
};
const App: React.FC = () => {
const [text, setText] = useState<string>("")
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setText(e.currentTarget.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
);
}
這裏給 onChange 方法定義了方法的類型,它是一個 ChangeEventHandler 的類型,並且作用的對象是一個 HTMLImnputElement 類型的標籤(input 標籤)。
五、HTML 標籤類型
1. 常見標籤類型
在項目的依賴文件中可以找到 HTML 標籤相關的類型聲明文件:
所有的 HTML 標籤的類型都被定義在 intrinsicElements 接口中,常見的標籤及其類型如下:
a: HTMLAnchorElement;
body: HTMLBodyElement;
br: HTMLBRElement;
button: HTMLButtonElement;
div: HTMLDivElement;
h1: HTMLHeadingElement;
h2: HTMLHeadingElement;
h3: HTMLHeadingElement;
html: HTMLHtmlElement;
img: HTMLImageElement;
input: HTMLInputElement;
ul: HTMLUListElement;
li: HTMLLIElement;
link: HTMLLinkElement;
p: HTMLParagraphElement;
span: HTMLSpanElement;
style: HTMLStyleElement;
table: HTMLTableElement;
tbody: HTMLTableSectionElement;
video: HTMLVideoElement;
audio: HTMLAudioElement;
meta: HTMLMetaElement;
form: HTMLFormElement;
那什麼時候會使用到標籤類型呢,上面第四部分的 Event 事件類型和事件處理函數類型中都使用到了標籤的類型。上面的很多的類型都需要傳入一個 ELement 類型的泛型參數,這個泛型參數就是對應的標籤類型值,可以根據標籤來選擇對應的標籤類型。這些類型都繼承自 HTMLElement 類型,如果使用時對類型類型要求不高,可以直接寫 HTMLELement。比如下面的例子:
<Button
type="text"
onClick={(e: React.MouseEvent<HTMLElement>) => {
handleOperate();
e.stopPropagation();
}}
>
<img
src={cancelChangeIcon}
alt=""
/>
取消修改
</Button>
其實,在直接操作 DOM 時也會用到標籤類型,雖然我們現在通常會使用框架來開發,但是有時候也避免不了直接操作 DOM。比如我在工作中,項目中的某一部分組件是通過 npm 來引入的其他組的組件,而在很多時候,我有需要動態的去個性化這個組件的樣式,最直接的辦法就是通過原生 JavaScript 獲取到 DOM 元素,來進行樣式的修改,這時候就會用到標籤類型。
來看下面的例子:
document.querySelectorAll('.paper').forEach(item => {
const firstPageHasAddEle = (item.firstChild as HTMLDivElement).classList.contains('add-ele');
if (firstPageHasAddEle) {
item.removeChild(item.firstChild as ChildNode);
}
})
這是我最近寫的一段代碼(略微刪改),在第一頁有個 add-ele 元素的時候就刪除它。這裏我們將 item.firstChild 斷言成了 HTMLDivElement 類型,如果不斷言,item.firstChild 的類型就是 ChildNode,而 ChildNode 類型中是不存在 classList 屬性的,所以就就會報錯,當我們把他斷言成 HTMLDivElement 類型時,就不會報錯了。很多時候,標籤類型可以和斷言(as)一起使用。
後面在 removeChild 時又使用了 as 斷言,爲什麼呢?item.firstChild 不是已經自動識別爲 ChildNode 類型了嗎?因爲 TS 會認爲,我們可能不能獲取到類名爲 paper 的元素,所以 item.firstChild 的類型就被推斷爲 ChildNode | null,我們有時候比 TS 更懂我們定義的元素,知道頁面一定存在 paper 元素,所以可以直接將 item.firstChild 斷言成 ChildNode 類型。
2. 標籤屬性類型
衆所周知,每個 HTML 標籤都有自己的屬性,比如 Input 框就有 value、width、placeholder、max-length 等屬性,下面是 Input 框的屬性類型定義:
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
accept?: string | undefined;
alt?: string | undefined;
autoComplete?: string | undefined;
autoFocus?: boolean | undefined;
capture?: boolean | string | undefined;
checked?: boolean | undefined;
crossOrigin?: string | undefined;
disabled?: boolean | undefined;
enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined;
form?: string | undefined;
formAction?: string | undefined;
formEncType?: string | undefined;
formMethod?: string | undefined;
formNoValidate?: boolean | undefined;
formTarget?: string | undefined;
height?: number | string | undefined;
list?: string | undefined;
max?: number | string | undefined;
maxLength?: number | undefined;
min?: number | string | undefined;
minLength?: number | undefined;
multiple?: boolean | undefined;
name?: string | undefined;
pattern?: string | undefined;
placeholder?: string | undefined;
readOnly?: boolean | undefined;
required?: boolean | undefined;
size?: number | undefined;
src?: string | undefined;
step?: number | string | undefined;
type?: string | undefined;
value?: string | ReadonlyArray<string> | number | undefined;
width?: number | string | undefined;
onChange?: ChangeEventHandler<T> | undefined;
}
如果我們需要直接操作 DOM,就可能會用到元素屬性類型,常見的元素屬性類型如下:
-
HTML 屬性類型:HTMLAttributes
-
按鈕屬性類型:ButtonHTMLAttributes
-
表單屬性類型:FormHTMLAttributes
-
圖片屬性類型:ImgHTMLAttributes
-
輸入框屬性類型:InputHTMLAttributes
-
鏈接屬性類型:LinkHTMLAttributes
-
meta 屬性類型:MetaHTMLAttributes
-
選擇框屬性類型:SelectHTMLAttributes
-
表格屬性類型:TableHTMLAttributes
-
輸入區屬性類型:TextareaHTMLAttributes
-
視頻屬性類型:VideoHTMLAttributes
-
SVG 屬性類型:SVGAttributes
-
WebView 屬性類型:WebViewHTMLAttributes
一般情況下,我們是很少需要在項目中顯式的去定義標籤屬性的類型。如果子級去封裝組件庫的話,這些屬性就能發揮它們的作用了。來看例子(來源於網絡,僅供學習):
import React from 'react';
import classNames from 'classnames'
export enum ButtonSize {
Large = 'lg',
Small = 'sm'
}
export enum ButtonType {
Primary = 'primary',
Default = 'default',
Danger = 'danger',
Link = 'link'
}
interface BaseButtonProps {
className?: string;
disabled?: boolean;
size?: ButtonSize;
btnType?: ButtonType;
children: React.ReactNode;
href?: string;
}
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> // 使用 交叉類型(&) 獲得我們自己定義的屬性和原生 button 的屬性
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLAnchorElement> // 使用 交叉類型(&) 獲得我們自己定義的屬性和原生 a標籤 的屬性
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps> //使用 Partial<> 使兩種屬性可選
const Button: React.FC<ButtonProps> = (props) => {
const {
disabled,
className,
size,
btnType,
children,
href,
...restProps
} = props;
const classes = classNames('btn', className, {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === ButtonType.Link) && disabled // 只有 a 標籤纔有 disabled 類名,button沒有
})
if(btnType === ButtonType.Link && href) {
return (
<a
className={classes}
href={href}
{...restProps}
>
{children}
</a>
)
} else {
return (
<button
className={classes}
disabled={disabled} // button元素默認有disabled屬性,所以即便沒給他設置樣式也會和普通button有一定區別
{...restProps}
>
{children}
</button>
)
}
}
Button.defaultProps = {
disabled: false,
btnType: ButtonType.Default
}
export default Button;
這段代碼就是用來封裝一個 buttom 按鈕,在 button 的基礎上添加了一些自定義屬性,比如上面將 button 的類型使用交叉類型(&)獲得自定義屬性和原生 button 屬性 :
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>
可以看到,標籤屬性類型在封裝組件庫時還是很有用的,更多用途可以自己探索~
六、工具泛型
在項目中使用一些工具泛型可以提高我們的開發效率,少寫很多類型定義。下面來看看有哪些常見的工具泛型,以及其使用方式。
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 具備以下特性:
-
從瀏覽器中創建 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;
}
};
需要注意,類型斷言並不是類型轉換,斷言成一個聯合類型中不存在的類型是不允許的。
再來看一個例子,在調用一個方法時傳入參數:data?.subjectId as number
除此之外,上面所說的標籤類型、組件類型、時間類型都可以使用斷言來指定給一些數據,還是要根據實際的業務場景來使用。
感悟:使用類型斷言真的能解決項目中的很多報錯~
5. 枚舉類型
枚舉類型在項目中的作用也是不可忽視的,使用枚舉類型可以讓代碼的擴展性更好,當我想更改某屬性值時,無需去全局更改這個屬性,只要更改枚舉中的值即可。通常情況下,最好新建一個文件專門來定義枚舉值,便於引用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/l4sV6nHT7tPsKpLx5bu-6w