菏澤網(wǎng)站建設(shè) 梧桐樹(shù)域名查詢(xún)?cè)L問(wèn)
本文根據(jù)日常開(kāi)發(fā)實(shí)踐,參考優(yōu)秀文章、文檔,來(lái)說(shuō)說(shuō) TypeScript
是如何較優(yōu)雅的融入 React
項(xiàng)目的。
溫馨提示:日常開(kāi)發(fā)中已全面擁抱函數(shù)式組件和 React Hooks
,class
類(lèi)組件的寫(xiě)法這里不提及。
前沿
- 以前有 JSX 語(yǔ)法,必須引入 React。React 17.0+ 不需要強(qiáng)制聲明 React 了。
import React, { useState } from 'react';// 以后將被替代成
import { useState } from 'react';
import * as React from 'react';
基礎(chǔ)介紹
基本類(lèi)型
- 基礎(chǔ)類(lèi)型就沒(méi)什么好說(shuō)的了,以下都是比較常用的,一般比較好理解,也沒(méi)什么問(wèn)題。
type BasicTypes = {message: string;count: number;disabled: boolean;names: string[]; // or Array<string>id: string | number; // 聯(lián)合類(lèi)型
}
聯(lián)合類(lèi)型
一般的聯(lián)合類(lèi)型,沒(méi)什么好說(shuō)的,這里提一下非常有用,但新手經(jīng)常遺忘的寫(xiě)法 —— 字符字面量聯(lián)合。
- 例如:自定義
ajax
時(shí),一般method
就那么具體的幾種:get
、post
、put
等。
大家都知道需要傳入一個(gè)string
型,你可能會(huì)這么寫(xiě):
type UnionsTypes = {method: string; // ? bad,可以傳入任意字符串
};
- 使用字符字面量聯(lián)合類(lèi)型,第一、可以智能提示你可傳入的字符常量;第二、防止拼寫(xiě)錯(cuò)誤。后面會(huì)有更多的例子。
type UnionsTypes = {method: 'get' | 'post'; // ? good 只允許 'get'、'post' 字面量
};
對(duì)象類(lèi)型
- 一般你知道確切的屬性類(lèi)型,這沒(méi)什么好說(shuō)的。
type ObjectTypes = {obj3: {id: string;title: string;};objArr: {id: string;title: string;}[]; // 對(duì)象數(shù)組,or Array<{ id: string, title: string }>
};
- 但有時(shí)你只知道是個(gè)對(duì)象,而不確定具體有哪些屬性時(shí),你可能會(huì)這么用:
type ObjectTypes = {obj: object; // ? bad,不推薦obj2: {}; // ? bad 幾乎類(lèi)似 object
};
- 一般編譯器會(huì)提示你,不要這么使用,推薦使用
Record
。
type ObjectTypes = {objBetter: Record<string, unknown>; // ? better,代替 obj: object// 對(duì)于 obj2: {}; 有三種情況:obj2Better1: Record<string, unknown>; // ? better 同上obj2Better2: unknown; // ? any valueobj2Better3: Record<string, never>; // ? 空對(duì)象/** Record 更多用法 */dict1: {[key: string]: MyTypeHere;};dict2: Record<string, MyTypeHere>; // 等價(jià)于 dict1
};
Record
有什么好處呢,先看看實(shí)現(xiàn):
// 意思就是,泛型 K 的集合作為返回對(duì)象的屬性,且值類(lèi)型為 T
type Record<K extends keyof any, T> = {[P in K]: T;
};
- 官方的一個(gè)例子
interface PageInfo {title: string;
}type Page = 'home' | 'about' | 'contact';const nav: Record<Page, PageInfo> = {about: { title: 'about' },contact: { title: 'contact' },// TS2322: Type '{ about: { title: string; }; contact: { title: string; }; hoem: { title: string; }; }' // is not assignable to type 'Record<Page, PageInfo>'. ...hoem: { title: 'home' },
};nav.about;
好處:
- 當(dāng)你書(shū)寫(xiě)
home
值時(shí),鍵入h
常用的編輯器有智能補(bǔ)全提示; home
拼寫(xiě)錯(cuò)誤成hoem
,會(huì)有錯(cuò)誤提示,往往這類(lèi)錯(cuò)誤很隱蔽;- 收窄接收的邊界。
函數(shù)類(lèi)型
- 函數(shù)類(lèi)型不建議直接給
Function
類(lèi)型,有明確的參數(shù)類(lèi)型、個(gè)數(shù)與返回值類(lèi)型最佳。
type FunctionTypes = {onSomething: Function; // ? bad,不推薦。任何可調(diào)用的函數(shù)onClick: () => void; // ? better ,明確無(wú)參數(shù)無(wú)返回值的函數(shù)onChange: (id: number) => void; // ? better ,明確參數(shù)無(wú)返回值的函數(shù)onClick(event: React.MouseEvent<HTMLButtonElement>): void; // ? better
};
可選屬性
- React props 可選的情況下,比較常用。
type OptionalTypes = {optional?: OptionalType; // 可選屬性
};
- 例子:封裝一個(gè)第三方組件,對(duì)方可能并沒(méi)有暴露一個(gè) props 類(lèi)型定義時(shí),而你只想關(guān)注自己的上層定義。
name
,age
是你新增的屬性,age
可選,other
為第三方的屬性集。
type AppProps = {name: string;age?: number;[propName: string]: any;
};
const YourComponent = ({ name, age, ...other }: AppProps) => (<div>{`Hello, my name is ${name}, ${age || 'unknown'}`} <Other {...other} /></div>
);
React Prop 類(lèi)型
- 如果你有配置
Eslint
等一些代碼檢查時(shí),一般函數(shù)組件需要你定義返回的類(lèi)型,或傳入一些React
相關(guān)的類(lèi)型屬性。
這時(shí)了解一些React
自定義暴露出的類(lèi)型就很有必要了。例如常用的React.ReactNode
。
export declare interface AppProps {children1: JSX.Element; // ? bad, 沒(méi)有考慮數(shù)組類(lèi)型children2: JSX.Element | JSX.Element[]; // ? 沒(méi)考慮字符類(lèi)型children3: React.ReactChildren; // ? 名字唬人,工具類(lèi)型,慎用children4: React.ReactChild[]; // better, 但沒(méi)考慮 nullchildren: React.ReactNode; // ? best, 最佳接收所有 children 類(lèi)型functionChildren: (name: string) => React.ReactNode; // ? 返回 React 節(jié)點(diǎn)style?: React.CSSProperties; // React styleonChange?: React.FormEventHandler<HTMLInputElement>; // 表單事件! 泛型參數(shù)即 `event.target` 的類(lèi)型
}
更多參考資料
函數(shù)式組件
熟悉了基礎(chǔ)的 TypeScript
使用 與 React
內(nèi)置的一些類(lèi)型后,我們?cè)撻_(kāi)始著手編寫(xiě)組件了。參考 前端進(jìn)階面試題詳細(xì)解答
- 聲明純函數(shù)的最佳實(shí)踐
type AppProps = { message: string }; /* 也可用 interface */
const App = ({ message }: AppProps) => <div>{message}</div>; // 無(wú)大括號(hào)的箭頭函數(shù),利用 TS 推斷。
- 需要隱式
children
?可以試試React.FC
。
type AppProps = { title: string };
const App: React.FC<AppProps> = ({ children, title }) => <div title={title}>{children}</div>;
- 爭(zhēng)議
React.FC
(orFunctionComponent
)是顯式返回的類(lèi)型,而"普通函數(shù)"版本則是隱式的(有時(shí)還需要額外的聲明)。React.FC
對(duì)于靜態(tài)屬性如displayName
,propTypes
,defaultProps
提供了自動(dòng)補(bǔ)充和類(lèi)型檢查。React.FC
提供了默認(rèn)的children
屬性的大而全的定義聲明,可能并不是你需要的確定的小范圍類(lèi)型。- 2和3都會(huì)導(dǎo)致一些問(wèn)題。有人不推薦使用。
目前 React.FC
在項(xiàng)目中使用較多。因?yàn)榭梢酝祽?#xff0c;還沒(méi)碰到極端情況。
Hooks
項(xiàng)目基本上都是使用函數(shù)式組件和 React Hooks
。
接下來(lái)介紹常用的用 TS 編寫(xiě) Hooks 的方法。
useState
- 給定初始化值情況下可以直接使用
import { useState } from 'react';
// ...
const [val, toggle] = useState(false);
// val 被推斷為 boolean 類(lèi)型
// toggle 只能處理 boolean 類(lèi)型
- 沒(méi)有初始值(undefined)或初始 null
type AppProps = { message: string };
const App = () => {const [data] = useState<AppProps | null>(null);// const [data] = useState<AppProps | undefined>();return <div>{data && data.message}</div>;
};
- 更優(yōu)雅,鏈?zhǔn)脚袛?/li>
// data && data.message
data?.message
useEffect
- 使用
useEffect
時(shí)傳入的函數(shù)簡(jiǎn)寫(xiě)要小心,它接收一個(gè)無(wú)返回值函數(shù)或一個(gè)清除函數(shù)。
function DelayedEffect(props: { timerMs: number }) {const { timerMs } = props;useEffect(() =>setTimeout(() => {/* do stuff */}, timerMs),[timerMs]);// ? bad example! setTimeout 會(huì)返回一個(gè)記錄定時(shí)器的 number 類(lèi)型// 因?yàn)楹?jiǎn)寫(xiě),箭頭函數(shù)的主體沒(méi)有用大括號(hào)括起來(lái)。return null;
}
- 看看
useEffect
接收的第一個(gè)參數(shù)的類(lèi)型定義。
// 1. 是一個(gè)函數(shù)
// 2. 無(wú)參數(shù)
// 3. 無(wú)返回值 或 返回一個(gè)清理函數(shù),該函數(shù)類(lèi)型無(wú)參數(shù)、無(wú)返回值 。
type EffectCallback = () => (void | (() => void | undefined));
- 了解了定義后,只需注意加層大括號(hào)。
function DelayedEffect(props: { timerMs: number }) {const { timerMs } = props;useEffect(() => {const timer = setTimeout(() => {/* do stuff */}, timerMs);// 可選return () => clearTimeout(timer);}, [timerMs]);// ? 確保函數(shù)返回 void 或一個(gè)返回 void|undefined 的清理函數(shù)return null;
}
- 同理,async 處理異步請(qǐng)求,類(lèi)似傳入一個(gè)
() => Promise<void>
與EffectCallback
不匹配。
// ? bad
useEffect(async () => {const { data } = await ajax(params);// todo
}, [params]);
- 異步請(qǐng)求,處理方式:
// ? better
useEffect(() => {(async () => {const { data } = await ajax(params);// todo})();
}, [params]);// 或者 then 也是可以的
useEffect(() => {ajax(params).then(({ data }) => {// todo});
}, [params]);
useRef
useRef
一般用于兩種場(chǎng)景
-
引用
DOM
元素; -
不想作為其他
hooks
的依賴(lài)項(xiàng),因?yàn)?ref
的值引用是不會(huì)變的,變的只是ref.current
。
- 使用
useRef
,可能會(huì)有兩種方式。
const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement | null>(null);
- 非 null 斷言
null!
。斷言之后的表達(dá)式非 null、undefined
function MyComponent() {const ref1 = useRef<HTMLElement>(null!);useEffect(() => {doSomethingWith(ref1.current);// 跳過(guò) TS null 檢查。e.g. ref1 && ref1.current});return <div ref={ref1}> etc </div>;
}
- 不建議使用
!
,存在隱患,Eslint 默認(rèn)禁掉。
function TextInputWithFocusButton() {// 初始化為 null, 但告知 TS 是希望 HTMLInputElement 類(lèi)型// inputEl 只能用于 input elementsconst inputEl = React.useRef<HTMLInputElement>(null);const onButtonClick = () => {// TS 會(huì)檢查 inputEl 類(lèi)型,初始化 null 是沒(méi)有 current 上是沒(méi)有 focus 屬性的// 你需要自定義判斷! if (inputEl && inputEl.current) {inputEl.current.focus();}// ? bestinputEl.current?.focus();};return (<><input ref={inputEl} type="text" /><button onClick={onButtonClick}>Focus the input</button></>);
}
useReducer
使用 useReducer
時(shí),多多利用 Discriminated Unions 來(lái)精確辨識(shí)、收窄確定的 type
的 payload
類(lèi)型。
一般也需要定義 reducer
的返回類(lèi)型,不然 TS 會(huì)自動(dòng)推導(dǎo)。
- 又是一個(gè)聯(lián)合類(lèi)型收窄和避免拼寫(xiě)錯(cuò)誤的精妙例子。
const initialState = { count: 0 };// ? bad,可能傳入未定義的 type 類(lèi)型,或碼錯(cuò)單詞,而且還需要針對(duì)不同的 type 來(lái)兼容 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();}
}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></>);
}
useContext
一般 useContext
和 useReducer
結(jié)合使用,來(lái)管理全局的數(shù)據(jù)流。
- 例子
interface AppContextInterface {state: typeof initialState;dispatch: React.Dispatch<ACTIONTYPE>;
}const AppCtx = React.createContext<AppContextInterface>({state: initialState,dispatch: (action) => action,
});
const App = (): React.ReactNode => {const [state, dispatch] = useReducer(reducer, initialState);return (<AppCtx.Provider value={{ state, dispatch }}><Counter /></AppCtx.Provider>);
};// 消費(fèi) context
function Counter() {const { state, dispatch } = React.useContext(AppCtx);return (<>Count: {state.count} <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button><button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button></>);
}
自定義 Hooks
Hooks
的美妙之處不只有減小代碼行的功效,重點(diǎn)在于能夠做到邏輯與 UI 分離。做純粹的邏輯層復(fù)用。
- 例子:當(dāng)你自定義 Hooks 時(shí),返回的數(shù)組中的元素是確定的類(lèi)型,而不是聯(lián)合類(lèi)型??梢允褂?const-assertions 。
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],而不是聯(lián)合類(lèi)型 (boolean | typeof load)[]
}
- 也可以斷言成
tuple type
元組類(lèi)型。
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>];
}
- 如果對(duì)這種需求比較多,每個(gè)都寫(xiě)一遍比較麻煩,可以利用泛型定義一個(gè)輔助函數(shù),且利用 TS 自動(dòng)推斷能力。
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]
}
擴(kuò)展
工具類(lèi)型
學(xué)習(xí) TS 好的途徑是查看優(yōu)秀的文檔和直接看 TS 或類(lèi)庫(kù)內(nèi)置的類(lèi)型。這里簡(jiǎn)單做些介紹。
- 如果你想知道某個(gè)函數(shù)返回值的類(lèi)型,你可以這么做
// foo 函數(shù)原作者并沒(méi)有考慮會(huì)有人需要返回值類(lèi)型的需求,利用了 TS 的隱式推斷。
// 沒(méi)有顯式聲明返回值類(lèi)型,并 export,外部無(wú)法復(fù)用
function foo(bar: string) {return { baz: 1 };
}// TS 提供了 ReturnType 工具類(lèi)型,可以把推斷的類(lèi)型吐出
type FooReturn = ReturnType<typeof foo>; // { baz: number }
- 類(lèi)型可以索引返回子屬性類(lèi)型
function foo() {return {a: 1,b: 2,subInstArr: [{c: 3,d: 4,},],};
}type InstType = ReturnType<typeof foo>;
type SubInstArr = InstType['subInstArr'];
type SubIsntType = SubInstArr[0];const baz: SubIsntType = {c: 5,d: 6, // type checks ok!
};// 也可一步到位
type SubIsntType2 = ReturnType<typeof foo>['subInstArr'][0];
const baz2: SubIsntType2 = {c: 5,d: 6, // type checks ok!
};
同理工具類(lèi)型 Parameters
也能推斷出函數(shù)參數(shù)的類(lèi)型。
- 簡(jiǎn)單的看看實(shí)現(xiàn):關(guān)鍵字
infer
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
T extends (...args: any) => infer R ? R : any;
的意思是 T 能夠賦值給 (...args: any) => any
的話,就返回該函數(shù)推斷出的返回值類(lèi)型 R
。
defaultProps
默認(rèn)值問(wèn)題。
type GreetProps = { age: number } & typeof defaultProps;
const defaultProps = {age: 21,
};const Greet = (props: GreetProps) => {// etc
};
Greet.defaultProps = defaultProps;
- 你可能不需要 defaultProps
type GreetProps = { age?: number };const Greet = ({ age = 21 }: GreetProps) => { // etc
};
消除魔術(shù)數(shù)字/字符
本人比較痛恨的一些代碼點(diǎn)。
- 糟糕的例子,看到下面這段代碼不知道你的內(nèi)心,有沒(méi)有羊駝奔騰。
if (status === 0) {// ...
} else {// ...
}// ...if (status === 1) {// ...
}
- 利用枚舉,統(tǒng)一注釋且語(yǔ)義化
// enum.ts
export enum StatusEnum {Doing, // 進(jìn)行中Success, // 成功Fail, // 失敗
}//index.tsx
if (status === StatusEnum.Doing) {// ...
} else {// ...
}// ...if (status === StatusEnum.Success) {// ...
}
- ts enum 略有爭(zhēng)議,有的人推崇去掉 ts 代碼依舊能正常運(yùn)行,顯然 enum 不行。
// 對(duì)象常量
export const StatusEnum = {Doing: 0, // 進(jìn)行中Success: 1, // 成功Fail: 2, // 失敗
};
- 如果字符單詞本身就具有語(yǔ)義,你也可以用字符字面量聯(lián)合類(lèi)型來(lái)避免拼寫(xiě)錯(cuò)誤
export declare type Position = 'left' | 'right' | 'top' | 'bottom';
let position: Position;// ...// TS2367: This condition will always return 'false' since the types 'Position' and '"lfet"' have no overlap.
if (position === 'lfet') { // 單詞拼寫(xiě)錯(cuò)誤,往往這類(lèi)錯(cuò)誤比較難發(fā)現(xiàn)// ...
}
延伸:策略模式消除 if、else
if (status === StatusEnum.Doing) {return '進(jìn)行中';
} else if (status === StatusEnum.Success) {return '成功';
} else {return '失敗';
}
- 策略模式
// 對(duì)象常量
export const StatusEnumText = {[StatusEnum.Doing]: '進(jìn)行中',[StatusEnum.Success]: '成功',[StatusEnum.Fail]: '失敗',
};// ...
return StatusEnumText[status];