首頁>技術>

一、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 來轉譯程式碼不同。下面簡單介紹幾個配置的作用與注意事項。

target

target 是用來配置 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 })]  },}
esModuleInteropallowSyntheticDefaultImports

在 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.CSSProperties

css-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 Componentsenhancer
interface 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

Injectors
interface 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 typescirptuseState
const [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>    </>  );}
useContext
type 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 typescript
yarn 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

11
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Python合集之Python字典(四)