Skip to main content

[TS] React with TypeScript

專案設置#

建立專案#

  1. 透過 create-react-app 產生專案
$ npx create-react-app my-app --template typescript
  1. 定義 tsconfig.json
  2. 定義 eslintrc.js
$ npx eslint --init
# 安裝完 eslint 後可能會與 create-react-app 的 ESLint 有版本衝突
# 先移除 package.json 中的 eslint
$ rm package-lock.json
$ rm -rf node_modules
$ npm install
  1. 定義 .prettierrc
    • 若要整合到 ESLint 需留意和 TypeScript 可能的衝突,可參考這篇的設定。

ESLint 設定#

參考設定:create-exposed-app:eslint-config-airbnb-typescript 的參考設定。

  • React Error : is declared but its value is never read:參考 Introducing the New JSX Transform,升級到 react v17 後,不需要 import React 的 ESLint 設定

@typescript-eslint/eslint-plugin#

錯誤處理:Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser#

@typescript-eslint/parser

出現錯誤訊息:

Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: .eslintrc.js.
The file must be included in at least one of the projects provided

這個錯誤的意思是該檔案(.eslintrc.js)並沒有被包含在 tsconfig 的設定(include)中。之所以會有這個錯誤是因為 @typescript-eslint/parser 會試著去解析這隻檔案。

根據要不要實際讓 ESLint 去解析這支檔案的需求不同會有不同的設定方式,可以參考下方的文件。

  • 最簡單的解決方式是把 .eslintrc.js 放到 .eslintignore 中,這樣 ESLint 就不會去解析 .eslintrc.js 這支檔案。
  • 如果需要 lint 這隻檔案,但不需要出現 type-aware linting,則可以使用 ESLint 提供的 overrides 設定(可參考這裡

參考#

eslint-config-airbnb-typescript#

eslint-config-airbnb-typescript:安裝與設定方式 @ Github

eslint-plugin-eslint-comments#

針對的是 ESLint 在檔案中提供的指令,例如 /* eslint-disable */

eslint prettier#

eslint-plugin-prettier 可以透過 ESLint 的規則來達到 prettier 的效果,為了要讓此 plugin 正確運作,最好可以把和 code formatting 有關的 ESLint 規則都關閉,只使用 ESLint 來確保程式碼品質、並偵測出可能的問題。要把和 code formatting 有關的 ESLint 規則都關閉,則可以使用 eslint-config-prettier 這個工具。

  • eslint-config-prettier: used for disable all formatting-related ESLint rules.
  • eslint-plugin-prettier:透過 ESLint 來達到 prettier 的效果
note

Prettier 的原則是,透過 Prettier 來編排程式碼(code formatting);透過 linter(例如,ESLint)來確保程式碼的品質(code-quality)及避免可能的 bug。

安裝:

npm install --save-dev eslint prettier eslint-plugin-prettier eslint-config-prettier

使用:

// .eslintrc.js
{
// 這會套用 eslint-config-prettier 設定好的規則
extends: ['plugin:prettier/recommended'] // 放在陣列中最後一個
}

在根目錄建立 .prettierrc 後,eslint-config-prettier 則預設就會去讀取這支檔案。

extends 中使用 plugin:prettier/recommend 後,即可省略下述設定:

// 使用 plugin:prettier/recommend 的話,即可省略下述設定
{
"extends": ["prettier"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error", // 預設就會讀 .prettierrc
"arrow-body-style": "off",
"prefer-arrow-callback": "off"
}
}

參考#

Lint Staged#

React Props with Type#

Basic#

interface Props {
text: string;
}
const Banner = ({ text }: Props) => {
return <h1>Hello, {text}</h1>;
};

children as Props#

keywords: React.ReactNode#

一般 children 建議使用 React.ReactNode,參考 react-typescript-cheatsheet

type BannerProps = { children: React.ReactNode };
const Banner = ({ children }: BannerProps) => {
return <h1>{children}</h1>;
};

CSS style as Props#

keywords: React.CSSProperties#
type BannerProps = { style?: React.CSSProperties };
const Banner = ({ style = {} }: BannerProps) => {
return <h1 style={style}>Hello React with TypeScript</h1>;
};
Banner.defaultProps = {
style: {},
};

事件#

針對 React 元件上的 DOM 事件,可以利用 VSCode 提供的 hint,只需將滑鼠移到事件上方即可。

例如,將滑鼠移到 onSubmit,就可以知道這裡面取得的 event 其型別會是 React.FormEvent<HTMLFormElement>

React event with TypeScript

同樣的,將滑鼠移到 onChange 事件上,就可以知道這個事件的 event 其型別會是 React.ChangeEvent<HTMLInputElement>

React event with TypeScript

或者,在該事件中先帶入一個空的 () ,再透過 paramter hint 即可看到提示:

React Event with TypeScript

知道 event 的型別後就可以撰該 event 的 handle function:

interface Props {
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
}

React Hooks with Type#

useState#

// useState<StateType>();
const [isOpen, setIsOpen] = useState<boolean>(false);
const [data, setData] = useState<DateType | null>(null);

useReducer#

keywords: useReducer<ReducerStateType, ReducerActionType>(reducer, defaultState), React.Dispatch<ReducerActionType>#
  • 只要 reducer function 的型別有訂清楚的話,TypeScript 會自動推導 useReducer 中的型別

    // 定義 reducer 中 state 的 type
    type StateType = { foo: string; bar: string };
    const initialState: StateType = { foo: 'foo', bar: 'bar' };
    s;
    // 定義 reducer 中 action 的 type
    type ActionType = {
    type: 'ACTION_A' | 'ACTION_B' | 'ACTION_C';
    payload: number; // 如果 payload 的型別都一樣
    };
    // reducer 的型別有定義好的話,useReducer 的地方就不用在定義
    const reducer = (state: StateType, action: ActionType) => {
    /* ... */
    };
    const App = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    return <div>{/* ... */}</div>;
    };
  • 若有需要將 dispatch 當作 props 傳給其他 component 的話,同樣將滑鼠移到 dispatch function 上就可以知道它的型別。一般來說 dispatch 的型別就是會 React.Dispatch<ReducerActionType>ReducerActionType 是自己取的 type 名稱):

    Screen Shot 2021-06-05 at 6.17.23 PM

  • 如果 dispatch 的 action 會有不同類型 payload 的情況,可以定義多個 type 再搭配使用 | ,如此可以確保某些 type 的 action 其 payload 一定要符合特定型別:

    type BasicAction = {
    type: 'ACTION_A' | 'ACTION_B';
    };
    type SetterAction = {
    type: 'ACTION_C';
    payload: number;
    };
    type ActionType = BasicAction | SetterAction;
  • 如果看到錯誤訊息 Argument of type 'FooBarState' is not assignable to parameter of type 'never'. 可以留意看看是不是 reducer function 中最後沒有回傳 state(所有 case 都不符合的情況):

    const reducer = (state: StateType, action: ActionType) => {
    if (action.type === 'ACTION_A') {
    return /* ... */;
    }
    if (action.type === 'ACTION_B') {
    return /* ... */;
    }
    if (action.type === 'ACTION_C') {
    return /* ... */;
    }
    // 最後如何果沒回傳 default case 的話,這裡會變成 never type
    // return state;
    };

useContext#

keywords: createContext<ContextType>()#

只要一開始 createContext 的型別有定好的話,TypeScript 就會自動推導 Context 的型別:

// theme-context.tsx
import { createContext, ReactNode } from 'react';
type Themes = {
[key: string]: React.CSSProperties;
};
const defaultTheme: Themes = {
light: {
backgroundColor: 'white',
color: 'black',
},
dark: {
backgroundColor: '#555',
color: 'white',
},
};
export const ThemeContext = createContext(defaultTheme);
export const ThemeProvider = ({ children }: { children: ReactNode }) => (
<ThemeContext.Provider value={defaultTheme}>{children}</ThemeContext.Provider>
);

但如果 Context 的初始值和實際的型別不同時,可以透過泛型的方式來明確告知 TypeScript 這個 context 的型別:

// 由於 context 預設的 state 是 null,所以需要透過泛型明確告知 TS 這個 context 的型別
export const ThemeContext = createContext<Themes>(null);

useContext + useReducer#

使用 useContext 搭配 useReducer 時有一個需要留意的概念是,context 只能在元件外被 create;但 dispatch 只能在元件內被取得,因此雖然我們會把 dispatch 這個 function 放在 context 中讓其他元件可以被使用,但 context 中的 dispatch 一定會有一段時間是 undefined

方式一:讓型別可以接受 null,但每次使用前需要判斷#

如果是在 createContext 的時候把預設值帶成 null,同時把 context 的型別設成像是下面這樣:

// 不建議這樣做,這會導致後續要使用 context 內的值都需要先判斷它不是 null
const themeContext = createContext<ThemeContextType | null>(null);

雖然 createContext 的時候沒有問題,但卻會導致後續要使用此 context 的值時都要先確保它不是 null 才能使用,將會變得麻煩很多。

方式二:使用 as,但有點太過人為#

另一種一種解決的方式是透過 TypeScript 的 as,透過人為的方式來告訴 TypeScript 這個型別就是 ...,但這種方式太人為了:

// 這種方式有點太人為了,不太建議
const themeContext = createContext<ThemeContextType>({} as ThemeContextType);

React Class Component With Type#

import { ChangeEvent, Component } from 'react';
type CounterProps = {
incident: string;
};
type CounterState = {
count: number;
};
class Counter extends Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = {
count: 0,
};
}
// ...
render() {
const { incident } = this.props;
const { count } = this.state;
return (
/* ... */
);
}
}
export default Counter;

React Pattern with TypeScript#

renderProps#

在 renderProps 的應用中,我們會把 Component 當成一個 Props 傳給另一個 Component 使用,這時候我們需要定義該 Component 的型別。

使用 React.ComponentType<ComponentProps> 可以用來定義說「這是一個 React Component,且這個 component 將能夠接收 ComponentProps 做為參數:

// https://frontendmasters.com/courses/react-typescript/reusable-props-interface/
import { ComponentType, useContext } from 'react';
export interface AdjustInputProps {
id: string;
label: string;
value: number;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export interface ColorAdjustmentProps {
// 表示 Adjustment 這個 props 會是一個 React Component
// 它可以接收 AdjustInputProps 作為 props
Adjustment: ComponentType<AdjustInputProps>;
}
/**
* ColorAdjustment 這個 Component 將可以接收名為 Adjustment 的 React Component 作為 props
*/
export const ColorAdjustment = ({ Adjustment }: ColorAdjustmentProps) => {
// ...
return (
<section className="color-sliders">
<Adjustment id="red-slider" label="Red" onChange={handleRedChange} value={red} />
<Adjustment id="green-slider" label="Green" onChange={handleGreenChange} value={green} />
</section>
);
};

要使用定義好的 ColorAdjustment 只需要:

// https://frontendmasters.com/courses/react-typescript/reusable-props-interface/
import { ColorAdjustment } from './ColorAdjustment';
import { ColorInput } from './ColorInput';
import { ColorSlider } from './ColorSlider';
const Application = () => {
return (
<main>
{/* 把 ColorInput 和 ColorSlide 這兩個 React Component 透過 props 傳進去 */}
<ColorAdjustment Adjustment={ColorInput} />
<ColorAdjustment Adjustment={ColorSlider} />
</main>
);
};
export default Application;

常見問題#

要使用 Types 還是 Interfaces#

總而言之,在使用上兩者沒有太大的差別,僅有稍微的差別:

  • 使用 type 比起 interface 有一個好處在於,用 type 定義的話 VSCode 會顯示型別內有哪些資訊,但用 interface 的話只會顯示 interface 的名稱:

    Types vs Interface

  • 用 interface 如果想要直接看到型別資訊也是可以的,只需在滑鼠移上去該 interface 後,按「Option」鍵(左側 Ctrl 右邊那顆)即可

    2

  • type 沒辦法增添屬性進去,但 interface 總是可以透過 extend 來擴充

建議可以:

  • 針對 React 的 Props 或 State 使用 type 來定義,如此可以在編輯器看到型別資訊,同時保有限制
  • 針對 public API 使用 interface,別人如果有需要的話可以擴充這個型別
Last updated on