一、Babel for transpiling, tsc for types
在 babel7 之前,推薦使用 ts-loader 或者 awesome-typescript-loader 進行 typescript 檔案的轉譯, ts-loader 負責將 typescript(es6) 轉譯成 javascript(es6) , babel-loader 負責將 javascript(es6) 轉譯成 javascript(es5) 並新增 polyfills 。
// webpack.config.jsmodule.exports = { ... module: { rules: [ { test: /\.tsx?$/, use: ['babel-loader','ts-loader'] } ] }}
babel7 之後 babel 已經有了處理 typescript 檔案的預置,因此只需要 babel-loader 就能完成 tsx? 檔案的所有轉譯功能。下面是react工程常用的 babel 配置, @babel/preset-typescript 負責 typescript 語法的轉譯, @babel/preset-react 負責 jsx 語法的轉譯, @babel/preset-env 負責 es6+ 及 polyfills 的處理。
// .babelrc.jsmodule.exports = { presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 } ], ['@babel/preset-typescript'], ['@babel/preset-react'] ]};
但是 babel 只負責程式碼的轉譯,型別檢查和生成型別宣告檔案的功能還是要透過 tsc 完成, tsconfig.json 定義了型別檢查的配置。
// tsconfig.json{ "compilerOptions": { //"target": "esnext", "lib": ["dom"], "jsx": "react", "strict": true, "baseUrl": "./", "paths": { "assets/*": ["client/assets/*"], "components/*": ["client/components/*"], "pages/*": ["client/pages/*"], "utils/*": ["client/utils/*"] }, "allowSyntheticDefaultImports": true, // "esModuleInterop": true }, "include": ["client/**/*"], "exclude": ["node_modules", "dist"]}
前面說到 babel 負責程式碼的轉譯, tsc 負責型別檢查,所以當使用 babel 來進行 tsc? 檔案的轉譯時,並不會使用 tsconfig.json 的配置, 因此這些配置不會影響編譯的產物,這一點與僅使用 tsc 來轉譯程式碼不同。下面簡單介紹幾個配置的作用與注意事項。
targettarget 是用來配置 tsc 轉譯輸出的程式碼版本,但是在我們配置的react專案裡,程式碼轉譯會由 babel 來完成, target 的功能等價於 @babel/preset-env 預置的 target 配置,通常為了方便維護,我們提倡使用獨立的 .browserslistrc 配置檔案。
// .browserslistrcandroid >= 4.4ios >= 9
paths
paths 與webpack裡的 alias 配置有些類似,但是 paths 的配置不會對映到最終的編譯產物中,而只是為了定位型別宣告檔案,為了使簡寫路徑能夠對映到最終的產物中,還需要在webpack中配置 alias ,如果要保證原始碼模組解析和型別解析都正常,兩者的配置缺一不可。為了讓兩邊的配置保持一致,減少手動配置,可以使用 tsconfig-paths-webpack-plugin 外掛。
// webpack.config.jsconst extensions = ['.js', '.jsx', '.ts', '.tsx'];module.exports = { ... resolve: { extensions, plugins: [new TsconfigPathsPlugin({ extensions })] },}
esModuleInterop與allowSyntheticDefaultImports在 typescript 專案中,引入模組常看到兩種寫法
import React from "react";// import * as React from "react";
在ESModule體系中,上面的兩種寫法明顯是有區別的,第一種寫法只匯出了模組 a 中的預設匯出;第二種寫法是 將a 模組中所有匯出重新命名為變數 A。
// a.tsexport const a = 1;export const b = 2;export default 3;
ESModule打包會被轉譯成CommonJS模組
"use strict";Object.defineProperty(exports, "__esModule", { //表示這是個esmodule轉來的 value: true});exports.default = exports.b = exports.a = void 0;var a = 1;exports.a = a;var b = 2;exports.b = b;var _default = 3;exports.default = _default;
第一種寫法匯入:
import a from "a";console.log('模組', a); // 3
轉譯後
var a = require('a');console.log('模組', a.default); //3
第二寫法匯入:
import * as a from "a";console.log('模組', a); // {a: 1, b: 2, default: 3, __esModule: true}
轉譯後
var a = require('a');console.log('模組', a); // {a: 1, b: 2, default: 3, __esModule: true}
可以看到,如果從ESModule匯入另一個ESModule,第一種寫法取的是匯出的 default 變數,第二種匯出取的是匯出的所有變數(包括 default )構成的物件。但是由於歷史原因,大多數第三方庫的只提供 CommonJS 模組,比如 React 。將上面的a模組改寫成CommonJS會發現一個問題,ESModule有預設匯出 export default 的概念,在CommonJS裡沒有,CommonJS匯出的物件相當於是 exports 這個物件,
"use strict";exports.default = exports.b = exports.a = void 0;var a = 1;exports.a = a;var b = 2;exports.b = b;var _default = 3;exports.default = _default;
當 esModuleInterop: false時,如果我們從ESModule匯入CommonJS, tsc 在轉譯原始碼的時候對CommonJS模組處理類似於ESModule
第一種寫法import a from "a"; /* console.log(a) // 3 */第二種寫法import * as a from "a"; /* console.log(a) //{a: 1, b: 2, default: 3} */
也就是說:
import a from "a" 預設匯入等價於 const a = require("a").default import * as a from "a" 命名匯入等價於 const a = require("a")現在的問題是什麼呢?
基本上所有的CommonJS模組都不會定義 exports.default ,比如ReactCommonJS如果直接匯出一個 function ,使用 import * as ... 命名匯入會不相容,命名匯入必須是一個物件以React庫為例,它的匯出如下:
'use strict';if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.min.js');} else { module.exports = require('./cjs/react.development.js');}
第一種寫法匯入:
import React from 'react'console.log(React)
轉譯後
var React = require('react');console.log('模組', React.default); // undefined
在ESModule使用 React 變數值為 undefined 。
第二種寫法匯入:
import * as React from 'react'console.log('模組', React);
轉譯後
var React = require('react');console.log('模組', React); // 正常
所以當 esModuleInterop: false時,必須使用第二種寫法才不會影響程式碼的執行,如果想第一種寫法也能正常執行,可以設定esModuleInterop: true
。 tsc 在轉譯程式碼的時候會幫我們做一些相容處理,抹平ESModule匯入ESModule和匯入CommonJS模組之間的差異。
/*第一種寫法*/import React from 'react'console.log(React)/*第二種寫法*/import * as React from 'reactconsole.log(React)
當開啟esModuleInterop後,上面的程式碼會被轉譯成
/*第一種寫法*/Object.defineProperty(exports, "__esModule", { value: true });var React = __importDefault(require("react"));console.log(React.default)/*第二種寫法*/Object.defineProperty(exports, "__esModule", { value: true });var React = __importStar(require("react"));console.log(React)
tsc 會引入兩個輔助函式來解決上面的兩個問題。
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });}) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k];}));var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v });}) : function(o, v) { o["default"] = v;});var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); // 把整個CommonJS的匯出掛到default屬性上,這樣即使CommonJS匯出的是function也能正常匯入 return result;};var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; // 把整個CommonJS的匯出掛到default屬性上};
舉個簡單的例子,當esModuleInterop=true
// a.jsmodule.exports.a = 1;module.exports.b = 2;
引入上面的CommonJS模組,列印結果如下
import a from 'a'console.log(a) // {a: 1, b: 2}import * as a from 'a'console.log(a) // {a: 1, b: 2, default: {a: 1, b: 2}}
這樣不管程式碼被轉成成 a.default 還是 a 都能正確訪問匯入物件的屬性。
實際react專案中,我們不會使用 tsc 轉譯 typescript 檔案,而是使用 @babel/preset-typescript 預置轉譯程式碼,該預置預設的 esModuleInterop 為 true ,即使 tsconfig 檔案裡設定為 false 也是不會生效的, 但是tsconfig裡必須配置 allowSyntheticDefaultImports為 true ,這是為了防止 tsc 做型別檢查的時候報錯,也可以只在tsconfig裡設定 esModuleInterop為 true ,當esModuleInterop為 true 時, allowSyntheticDefaultImports也會自動取 true 。 babel 也會引入兩個類似的輔助匯入方法,分別是 _interopRequireWildcard 和 _interopRequireDefault 。
在開發過程中,為了實時檢查型別問題,可以執行 yarn type-check:watch ,不過更推薦使用webpack外掛 fork-ts-checker-webpack-plugin 。另外也提倡在提交程式碼時攔截型別錯誤。
{ ... "scripts": { "type-check": "tsc --noEmit", "type-check:watch": "yarn type-check --watch" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,ts,jsx,tsx,json,md}": [ "prettier --write" ], "*.{ts,tsx}": [ "eslint" ], "*.{ts, tsx}": [ "bash -c \"tsc -p ./tsconfig.json --noEmit\"" ] }}
二、Components with typescript1. React - Type-Definitions CheatsheetReact.FC<Props> | React.FunctionComponent<Props>函式式元件
const MyComponent: React.FC<Props> = ...
React.Component<Props, State>
class component
class MyComponent extends React.Component<Props, State> { ...
React.ComponentType<Props>
React.FC | React.Component 常用於高階元件
const withState = <P extends WrappedComponentProps>( WrappedComponent: React.ComponentType<P>,) => { ...
ReactText、ReactElement、JSX.Element、ReactChild、ReactNode
type ReactText = string | number; interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null;}type ReactChild = ReactElement | ReactText;interface ReactNodeArray extends Array<ReactNode> {}type ReactFragment = {} | ReactNodeArray;type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { } ... }}
ReactElement: React.createElement方法的返回值 ,值為一個物件
const elementOnly: React.ReactElement = <div /> || <MyComponent />;
JSX.Element: tsc推斷jsx時使用的型別,在React專案中等價於React.ReactElement<any, any> ,但是在React專案中並不推薦使用
const elementOnly = <div /> || <MyComponent />; // JSX.Element
ReactNode: 表示所有可能的React節點
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;const Component = ({ children: React.ReactNode }) => ...
React.CSSPropertiescss-in-js 樣式型別
const styles: React.CSSProperties = { flexDirection: 'row', ...const element = <div style={styles} ...
React.xxxxEventHandler<HTMLXXXElement>、React.XXXEvent<HTMLXXXElement>
React.xxxxEventHandler事件處理函式型別 (EventHandler>ReactEventHandler>ChangeEventHandler、MouseEventHandler...)
React.xxxxEvent 事件物件型別 (ChangeEvent、MouseEvent...)
const Input = () => { const [text, setText] = useState(''); const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { setText(e.target.value); }; // const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { // setText(e.target.value) // }; return <input type="text" onChange={handleChange} />;};
2. Function Components首先,可以把函式式元件當做一個普通的函式
interface Props { label: string;}const App = (props: Props) => { return <div>{props.label}</div>;};ReactDOM.render(<App label="test" />, document.getElementById('app-root'));
react庫本身也定義了函式式元件的型別
type SFC<P = {}> = FunctionComponent<P>;type StatelessComponent<P = {}> = FunctionComponent<P>;type FC<P = {}> = FunctionComponent<P>;interface FunctionComponent<P = {}> { (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null; propTypes?: WeakValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string;}
SFC 和 StatelessComponent 已經被遺棄不在推薦使用,建議顯式定義成 React.FC ,
const App: React.FC<Props> = (props) => { return <div>{props.label}</div>;};
與普通函式相比, React.FC 做了更多的規範
規範了函式的返回值必須返回 ReactElement 或者 null
const App = (props: Props) => { // 這裡不會有型別錯誤 return props.show && <div>{props.label}</div>;};ReactDOM.render(<App label="test" show={false} />, document.getElementById('app-root')); // 使用時會有型別錯誤// const App: React.FC<Props> = (props) => { // 定義時就會有型別錯誤,// return props.show && <div>{props.label}</div>;// };// ReactDOM.render(<App label="test" show={false} />, document.getElementById('app-root'));
補充閱讀:
Why do the render methods of class components return ReactNode, but function components return ReactElement
隱式添加了 children 屬性 type PropsWithChildren<P> = P & { children?: ReactNode };
interface Props { label: string;}const App: React.FC<Props> = ({ label, children }) => { return ( <div> {label}:{children} // 不需要在Props中定義,就能使用 </div> );};ReactDOM.render( <App label="test"> <span>text</span> </App>, document.getElementById('app-root'));
interface Props { label: string; children?: React.ReactNode; // 顯式定義}const App = ({ label, children }: Props) => { return ( <div> {label}:{children} </div> );};
對 propTypes 、 contextTypes 、 defaultProps 、 dispalyName 等靜態屬性型別做了規範。Higher-Order Componentsenhancerinterface WithLoadingProps { loading: boolean;}const withLoading = <P extends object>( Component: React.ComponentType<P>): React.FC<P & WithLoadingProps> => ({ loading, ...props }) => loading ? <div>loading</div> : <Component {...(props as P)} />;interface PageProps { label: string;}const Page = ({ label }: PageProps) => { return <div>{label}</div>;};const App = withLoading<PageProps>(Page);ReactDOM.render(<App label="test" loading={true} />, document.getElementById('app-root'));
https://github.com/Microsoft/TypeScript/issues/28938
Injectorsinterface InjectedProps { style: CSSProperties;}const withStyle = (style: CSSProperties) => { return <P extends InjectedProps>( Component: React.ComponentType<P> ): React.FC<Omit<P, keyof InjectedProps>> => (props) => ( <Component {...(props as P)} style={style} /> // <Component style={style} {...(props as P)} /> // 'style' is specified more than once, so this usage will be overwritten.ts(2783) // This spread always overwrites this property. const newProps = { ...props, style } as P; return <Component {...newProps} />; );};interface PageProps extends InjectedProps { label: string;}const Page: React.FC<PageProps> = ({ label, style }) => { return <div style={style}>{label}</div>;};const App = withStyle({ color: 'red', fontSize: '20px' })(Page);ReactDOM.render(<App label="test" />, document.getElementById('app-root'));
三、Hooks with typescirptuseStateconst [state, setState] = useState({ foo: 1, bar: 2,}); // 自動推斷為 {foo: number, bar: number}const someMethod = (obj: typeof state) => { };
interface Book { id: number; title: string; price: number;}const App = () => { // const [book, setBook] = useState<Book | null>(null); const [book, setBook] = useState<Book>(null!); useEffect(() => { setTimeout(setBook, 1000, { id: 1, title: '必修一' }); }, []); return book && <div>標題: {book.title}</div>;};
useRef
function TextInputWithFocusButton() { const inputEl = useRef<HTMLInputElement>(null); const onButtonClick = () => { if (inputEl?.current) { inputEl.current.focus(); } }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> );}function TextInputWithFocusButton() { const inputEl = useRef<HTMLInputElement>(null!); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> );}
useContexttype Theme = 'light' | 'dark';const ThemeContext = createContext<Theme>('dark');const App = () => ( <ThemeContext.Provider value="dark"> <MyComponent /> </ThemeContext.Provider>)const MyComponent = () => { const theme = useContext(ThemeContext); return <div>The theme is {theme}</div>;}
自定義hooks
const useCountdown = (initCount: number) => { const [count, setCount] = useState(initCount); useEffect(() => { if (count === 0) { return; } const timer = setTimeout(() => setCount(count - 1), 1000); return () => clearInterval(timer); }, [count]); return [count, setCount] as const;};const useCountdown: (initCount: number) => readonly [number, React.Dispatch<React.SetStateAction<number>>] // 元組型別const useCountdown: (initCount: number) => (number | React.Dispatch<React.SetStateAction<number>>)[] // 預設推斷的是陣列型別
四、React state management libraries with typescript1. React-model with typescript4.0.0以下版本// model/test.tsimport { Model } from "react-model";const initialState = { counter: 0, light: false, response: {}};export interface StateType { counter: number; light: boolean; response: { code?: number; message?: string; };}interface ActionsParamType { increment: number; // payload引數的型別 openLight: undefined; get: undefined;} // You only need to tag the type of params here !const model: ModelType<StateType, ActionsParamType> = { // react-model的型別宣告檔案不是以模組匯出的,所以是全域性可見 actions: { increment: async (payload, { state }) => { return { counter: state.counter + (payload || 1) }; }, openLight: async (_, { state, actions }) => { await actions.increment(1); // You can use other actions within the model await actions.get(); // support async functions (block actions) actions.get(); await actions.increment(1); // + 1 await actions.increment(1); // + 2 await actions.increment(1); // + 3 as expected ! return { light: !state.light }; }, get: async () => { await new Promise((resolve, reject) => setTimeout(() => { resolve(void 0); }, 3000) ); return { response: { code: 200, message: `${new Date().toLocaleString()} open light success` } }; } }, state: initialState};export default Model(model);
// model/index.tsimport { Model, actionMiddlewares, middlewares } from "react-model";import Test from "./test";export const models = { Test};// remove consoleDebugger middleware.const consoleDebuggerMiddlewareIndex = actionMiddlewares.indexOf(middlewares.consoleDebugger);actionMiddlewares.splice(consoleDebuggerMiddlewareIndex, 1);export const { getInitialState, useStore, getState, actions } = Model(models);
// pages/test.tsimport { useStore } from "models/index";export default () => { const [test, actions] = useStore("Test"); // test 自動推斷成StateType return ...};
4.0.0及以上版本, 目前還沒有釋出正式版,v4.0.0-rc.2
// model/test.tsconst initialState = { counter: 0, light: false, response: {}};export interface StateType { counter: number; light: boolean; response: { code?: number; message?: string; };}interface ActionsParamType { increment: number; openLight: undefined; get: undefined;} // You only need to tag the type of params here !const model: ModelType<StateType, ActionsParamType> = { ...};export default model; // 匯出時不需要Model處理
import { useStore } from "models/index";export default () => { const [test, actions] = useStore("Test"); // test 自動推斷成StateType const [counter, actions] = useStore("Test", (state) => state.counter); // counter 自動推斷成 number型別 return ...};
2. Redux with typescript
reudcers/book.tsimport { Reducer } from 'redux';export enum ActionTypes { LOADING = 'BOOK/LOADING', GET_BOOKS = 'BOOK/GET_BOOKS',}export interface Book { id: number; name: string; title: string; cover: string;}export interface IBooksLoadingAction { type: ActionTypes.LOADING; loading: boolean;}export interface IGetAllBooksAction { type: ActionTypes.GET_BOOKS; books: any[];}
// store/index.tsimport { combineReducers } from 'redux';import { useSelector, TypedUseSelectorHook } from 'react-redux';import Book from './book';export const rootReducer = combineReducers({ Book});export type RootState = ReturnType<typeof rootReducer>;export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
import { ThunkAction } from 'redux-thunk';import { Dispatch, ActionCreator, AnyAction } from 'redux';import { IGetAllBooksAction, IBooksLoadingAction, BookState, Book, ActionTypes} from '../reducers/book';import axios from 'axios';const loading: ActionCreator<IBooksLoadingAction> = (loading) => ({ type: ActionTypes.LOADING, loading});export const getAllBooks: ActionCreator<ThunkAction< Promise<AnyAction>, BookState, null, IGetAllBooksAction>> = () => { return async (dispatch: Dispatch) => { dispatch(loading(true)); const { data } = await axios.get<Book[]>('/api/books'); dispatch(loading(false)); return dispatch({ type: ActionTypes.GET_BOOKS, books: data }); };};
import React, { useEffect } from 'react';import { useDispatch } from 'react-redux';import { getAllBooks } from '../../actions/book';import { useTypedSelector } from 'store/index';export default () => { const dispatch = useDispatch(); const books = useTypedSelector((state) => state.Book.books); useEffect(() => { dispatch(getAllBooks()); }, []); return ...};
五、Axios with typescript
axios庫的型別定義比較簡單,可以自行檢視index.d.ts,下面截取了我們常用的部分
...export interface AxiosRequestConfig { url?: string; method?: Method; baseURL?: string; transformRequest?: AxiosTransformer | AxiosTransformer[]; transformResponse?: AxiosTransformer | AxiosTransformer[]; headers?: any; params?: any; paramsSerializer?: (params: any) => string; data?: any; timeout?: number; ...}export interface AxiosResponse<T = any> { data: T; status: number; statusText: string; headers: any; config: AxiosRequestConfig; request?: any;}export interface AxiosError<T = any> extends Error { config: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse<T>; isAxiosError: boolean; toJSON: () => object;}export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {}...export interface AxiosInterceptorManager<V> { use(onFulfilled?: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number; eject(id: number): void;}export interface AxiosInstance { (config: AxiosRequestConfig): AxiosPromise; (url: string, config?: AxiosRequestConfig): AxiosPromise; defaults: AxiosRequestConfig; interceptors: { request: AxiosInterceptorManager<AxiosRequestConfig>; response: AxiosInterceptorManager<AxiosResponse>; }; getUri(config?: AxiosRequestConfig): string; request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>; get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; head<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; options<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>; put<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>; patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;}...
常用的請求方法的型別定義都是類似下面的介面。
method<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
axios相關的大部分型別都能推匯出來,但是請求的響應還是需要業務自己定義。
enum Code { SUCCESS = 0, FAILD = 1}interface Res<T> { code: Code; data: T;}interface Error {// 假設是restful api, 後端返回非200狀態碼 errCode: number; errMsg: string;}interface Product { id: number; name: string;}const App = () => { const [products, setProducts] = useState<Product[]>([]); useEffect(() => { axios .get<Res<Product[]>>('/mock') .then((response) => { // http狀態碼2xx res自動推斷出的型別: AxiosResponse<Res<Product>> console.log(response.status); console.log(response.statusText); console.log(response.headers); console.log(response.config); setProducts(response.data.data); }) .catch((error: AxiosError<Error>) => { // http狀態碼非2xx err型別不能推斷出來,預設為any,需要顯式定義 console.log(error.code, error.toJSON(), error.response?.data.errMsg); }); }, []); return <div>{products.map(({ name }) => name)}</div>;};
如果將請求改成 async 和 await 形式,會遇到一些新的問題。
function isAxiosError<T = any>(error: any): error is AxiosError<T> { return error && (error as AxiosError).isAxiosError;}const App = () => { const [products, setProducts] = useState<Product[]>([]); useEffect(() => { (async () => { try { const response = await axios.get<Res<Product[]>>('/mock'); console.log(response.status); console.log(response.statusText); console.log(response.headers); console.log(response.config); setProducts(response.data.data); } catch (error: unknown) { // catch子句變數的型別只能為'any' or 'unknown',ts4.0以上建議顯式定義為'unknown'型別,這樣做的好處是在使用時必須再次明確型別 console.log(error.code, error.toJSON(), error.response?.data.errMsg); // 會有型別錯誤 if (isAxiosError<Error>(error)) { // User-Defined Type Guards console.log(error.code, error.toJSON(), error.response?.data.errMsg);// 沒有型別錯誤 } else { console.log(error); } } })(); }, []); return <div>{products.map(({ name }) => name)}</div>;};
六、Styled-components with typescriptyarn add @types/styled-components
interface BtnProps { size: 'small' | 'large';}const Button = styled.button<BtnProps>` height: ${(props) => (props.size === 'small' ? '24px' : '40px')};`;ReactDOM.render(<Button size="small">test</Button>, document.getElementById('app-root'));
interface OriginalButtonProps { className?: string; htmlType: 'submit' | 'reset' | 'button';}const OriginalButton: React.FC<OriginalButtonProps> = ({ children, className, htmlType, /*type */ }) => { // console.log(type) return <button className={className} type={htmlType}>{children}</button>;};interface BtnProps { type: 'primary' | 'danger'; size: 'small' | 'large';}const Button = styled(OriginalButton)<BtnProps>` height: ${(props) => (props.size === 'small' ? '24px' : '40px')}; background-color: ${(props) => (props.type === 'primary' ? 'blue' : 'red')};`;ReactDOM.render(<Button size="large" type="primary" htmlType="submit">test</Button>, document.getElementById('app-root'));
const Button = styled(({ size, type,...rest }) => <OriginalButton {...rest} />)<BtnProps>` height: ${(props) => (props.size === 'small' ? '24px' : '40px')};`;
七、react-router
<Router basename="/xxx"> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route exact path="/article/:id"> <ArticlePage /> </Route> </Switch> </Suspense></Router>
import { useParams } from 'react-router-dom';export default () => { const { id } = useParams<{ id: string }>(); // 路由引數只能是string或者undefined型別 return ...};
<Route exact path="/article/:id" component={ArticlePage} />
<Route exact path="/article/:id" render={(routeProps) => <ArticlePage {...routeProps} ownKey="article" />} />
import { RouteComponentProps } from 'react-router-dom';type Props = RouteComponentProps<{ id: string }> // 路由引數只能是string或者undefined型別const ArticlePage: React.FC<Props> = (props) => { const id = props.match.params.id; ...} interface Props extends RouteComponentProps<{ id: string }> { ownKey: string;}
import { useLocation } from 'react-router-dom';function useQuery() { return new URLSearchParams(useLocation().search);}
參考:
https://react-typescript-cheatsheet.netlify.app/
https://github.com/piotrwitek/react-redux-typescript-guide#react--redux-in-typescript---complete-guide