--- tags: react, TypeScript --- # TypeScript 菜雞:chicken:小分享 在閱讀以下關於使用TypeScript的經驗時,或許你會需要先初步了解TypeScript為何物,官方網站: [TypeScript](https://www.typescriptlang.org/docs/handbook/basic-types.html) 而我偏好使用type勝於interface,因此以下大多使用type <!-- 1. 簡介 2. 環境設置 / 套件 3. with React(create react app --typescript) 4. 文件架構 5. js -> ts 使用後心得 / 問題(約二個月) 6. TypeScript work flow --> --- ## 1. 簡介 ### TypeScript: JavaScript的一個嚴格超集,簡單來講就是在JavaScript的基礎上,加上type, interface, class...等的型別定義. 因此特色是JavaScript所沒有的靜態型別,透過型別來解決JavaScript的弱型別問題,在web大型專案的好處會因為其TypeScript的強型別而更有優勢. --- 透過簡單的function範例,馬上來看兩者的差別 在==JavaScript==中,如果要加總數字,你即便給的是字串也不會有問題,function照樣把字串加總起來,但就可能不會是你預期的答案 而==TypeScript==中,透過在參數定義型別的方式,強迫參數一定要是number,則typeScript在compile的時候就會檢查使用者給的變數,而在compile後,或是vsCode你有用eslint/tslint,就會出現error提醒你的變數用錯了! --- 簡單程式碼範例比較: ```typescript= //javaScript function sumNumbers(a, b) { return a + b; } console.log(sumNumbers('1' + '2')) //"12" 沒問題,但是變成string相加! //typeScript function sumNumbers(a: number, b: number) { return a + b; } //error TS2345: Argument of type 'string' is not assignable to parameter of type 'number' console.log(sumNumbers('1' + '2')) //ok console.log(sumNumbers(1 + 2)) //3 ``` --- ## 2. 從頭開始的基本環境設置(from scractch with react) 以下皆為基礎設定,額外的設定需個別查看module的文檔 範例檔案結構(依照專案設定會有所不同,以下僅為基礎示範): ``` |-tsconfig.json |-webpack.config.js |-src |-index.tsx |-index.html |-components |-... |-package.json |-yarn.lock ``` --- module安裝: ```typescript= //core modules --- yarn add react react-dom typescript //dev modules --- //webpack yarn add --dev webpack webpack-cli webpack-dev-server //@types yarn add --dev @types/react @types/react-dom //loaders(也可用其他typescript loader), plugins yarn add --dev awesome-typescript-loader html-webpack-plugin ``` --- webpack configs: ```typescript= const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.tsx', module: { rules: [ { test: /\.tsx?$/, //使用awesome-typescript-loader,不用babel...等 use: 'awesome-typescript-loader', exclude: /node_modules/ } ] }, resolve: { extensions: [ '.tsx', '.ts', '.js' ] }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html' }) ] } ``` --- tsconfig.json: ```json= { "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "allowJs": true, "module": "esnext", "moduleResolution": "node", //because react uses csstype module "allowSyntheticDefaultImports": true, "target": "es5", "jsx": "react" } } ``` ==allowSyntheticDefaultImports== true/false差別: ```typescript= //在default import 會有差別 //true import React from 'react' //false import * as React from 'react' ``` --- index.tsx ```typescript= import React from 'react' import { render } from 'react-dom' const Component = () => { return ( <div> {'HI :)'} </div> ) } export default Component render(<Component />, document.getElementById('root')) ``` --- package.json ```json= { //..., "scripts": { "start": "webpack-dev-server --open --watch", "build": "webpack" } } ``` --- 啟動專案(localhost) ``` npm start ``` --- ## 3. VScode相關設置 直接用eslint搭配TypeScipt 在vsCode的==User/settings.json==增加以下設定 ```json= "eslint.validate": [ "javascript", "javascriptreact", // ts { "language": "typescript", "autoFix": true }, // tsx { "language": "typescriptreact", "autoFix": true }, ] ``` --- ### 安裝好了node modules卻無法import?(@types) 明明module都裝好了,可是import時找不到? 這時候就是 ==@types==的問題了,coding時在vsCode中從modules引入的東西,會提示你為哪種型別,function該怎麼用,這個就是該module的@types檔(通常是index.d.ts)提供的型別了,而在.ts/.tsx中import的modules,都是從@types而來,然而有些modules會沒有撰寫@types的型別檔,因此就會辨識為無此module並且無法正確import. --- 那麼會有以下兩種情況: 有/沒有 @types 的module 有@types可以裝: ```typescript= //以 react 做舉例 yarn add @types/react //這樣在.ts/.tsx才可以正確引入react ``` --- 沒有@types可以裝: 1. 自己寫types檔 以someLibrary這個module做舉例 像是以下function引入時失敗,表示someLibrary沒有type檔 ```typescript= // import failed! module not found... import { sum } from 'someLibrary' ``` --- 2. 自訂type檔 以新建my-types為自訂types檔的資料夾做為舉例 資料結構 ``` ... |- src |- my-types |- someLibrary |- index.d.ts ``` 則按照其module的function去撰寫types ```typescript= //my-tyes/someLibrary/pesindex.d.ts decalare module 'someLibrary' { //import failed的那個module的名稱 export type sum = (a: number, b: number) => number //根據該function } ``` --- 3. 設定tsconfig.json中的typeRoots ```typescript= //tsconfig.json { "compilerOptions": { ..., "typeRoots": [ "./my-types" //這邊加上自訂types檔的路徑,typescript就會使用這些types ] } } ``` --- ## 4. 搭配React使用(含React專案之相關設定) ### 使用create react app馬上設定好基本環境(省去webpack, jest...等的設定) ```typescript= npx create-react-app my-app --typescript # or yarn create react-app my-app --typescript ``` ### 其他常用module的設定 storybook: 修改config & webpack.config.js ```typescript= // .storybook/config.ts ... const req = require.context("../src", true, /.stories.tsx$/); //改為.tsx ... // 需安裝awesome-typescript-loader的module // .storybook/webpack.config.js module.exports = ({ config }) => { config.module.rules.push({ test: /\.(ts|tsx)$/, use: [ { loader: require.resolve('awesome-typescript-loader'), }, // Optional // { // loader: require.resolve('react-docgen-typescript-loader'), // }, ], }); config.resolve.extensions.push('.ts', '.tsx'); return config; }; ``` --- jest: setupTests.ts 及 CLI指令 ```typescript= // src/setupTests.ts //如果有用 enzyme import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() }); //jest相關設定 module.exports = { collectCoverageFrom: [ "src/**/*.{ts,tsx}", "!**/stories/**", "!**/node_modules/**" ], }; ``` ``` //test coverage //需要加上 --watchAll=false react-scripts test --watchAll=false --coverage" ``` --- ## 5. React with TypeScript 工作流 ==主要會有幾種狀況:== >1. 先只有畫面(wireframe/彩稿/user story...等) >2. 先只有API >3. API和畫面皆有 --- 1. 先只有畫面(wireframe/彩稿):components based 透過先定義component自身的type開始: ```typescript= type Props = { width: number height: number } const Box = ({ width, height }: Props) => <div style={{width, height}} /> <Box width={20} height={20} /> //OK <Box width={20} /> //error: type不符合... //如此一來,即可清楚定義一個components的props, //透過約束該props值來保證components的使用正確 ``` --- 2. 先只有API :API based 先從定義API的type開始 ```typescript= interface API { id: string name: string value: number } const Box = ({ name, value }: API) => <div style={{width: value * 10}}>{name}</div> //直接透過API定義components的props ``` --- 3. API和畫面皆有 先透過components/API去觀察其資料面貌,以小至大來定義types,找出可以共用的types,以減少維護複雜度 :+1: ```typescript= //components import { User } from '...' const UserBox = (props: User) => ( <div>{props.user}: {props.status}</div> ) //API //原本API type type Article = { user: string status: string content: string likes: number } //拆解共用 >> export type User = { user: string status: string } type Article = User & { content: string likes: number } ``` --- 4. custom types: 當types被共用或是常用到時,歸納types用起來會更方便且更容易修改 my-types/... ```typescript= //常用的ID type export type ID = number ``` components / functions... ```typescript= function filterMinId = (id: ID, min: number) => { return id >= min //透過約束ID來做型別判斷 } const BoxWithId = (props: { id: ID }) => <div style={{width: id * 10}}>{id}</div> ``` --- ## 6. TypeScript使用後與JavaScript比較,以及遷移的過程 * ### types的靈活度 前面提到共用types另外獨立出來,即是和types的靈活度有關,一旦type定義的型別越多,則components/functions更難共用其types,且可能會更難做測試(耦合程度可能比較高),由以下兩個例子去比較: --- #### 例子A:(不做type處理) 如果是依賴上游(意即源頭,像是API或是其他的結果...,就像以下例子)的types,則上游types一旦更改了,那麼就需要一個個手動更改! ```typescript= type API = { id: string name: string serial: number gender: 1 | 2 content: string } //以下為依賴API的components/functions type Props = { name: string content: string } const User = (props: Props) => <Box {...props} /> function getGender(gender: 1 | 2) { return gender === 1 ? 'man' : 'woman' } ``` --- #### 例子B:(type已經整理/事先規劃好type) 如果將會共用或常用的types事先分離(專案越早期越好),之後就會省下修改和檢查的時間,且更好維護和測試! ```typescript= //共用 type User = { name: string content: string } type Gender = 1 | 2 type GenderStr = 'man' | 'woman' //組合 type API = User & { id: string serial: number gender: Gender } //以下為依賴API的components/functions type Props = User const User = (props: Props) => <Box {...props} /> const user_mockData: User = { //更好做mock for tests name: '', content: '' } function getGender(gender:Gender): GenderStr { return gender === 1 ? 'man' : 'woman' } ``` --- * ### 測試驅動開發 & 組件驅動開發: TDD(Test Driven Development) & CDD(Components Driven Development) 前面提到關於優化或是整理types,都是以兩者的原則為基礎! 以下提到的正例和反例,皆是根據範例的比較而來,僅為較遵守以及較為不遵守原則,畢竟沒有以量化的標準去區分 --- TDD: 以通過測試及撰寫測試為前提,一旦有程式可以進行測試,則以測試優先執行,測試完成後再接著下一步,以這樣的方式來開發整個專案 --- CDD: 以個別組件為基礎單元,整個頁面的UI就會是以個別單元組件去組合(react 通常會配合 storybook等module去配合呈現個別組件),專案的畫面即是以此呈現 --- TDD 與非 TDD簡單舉例(僅為個人習慣寫法的舉例): * 非TDD習慣寫法: function就會有==3x2x2=12種==的情況去做測試 * 而採用TDD習慣寫法: 將function複雜度降低,則只有==3+2+2=7==種情況去做測試 ```typescript= //非TDD的習慣寫法 function calNumbers(a: number, b: number, c?: number=1) { const sum = a > b ? a + b : a < b ? a - b : 0 const sum2 = sum > a ? sum + a : sum + b return sum2 > c ? sum2 * c : sum2 } //TDD習慣寫法,如果個別function有更改,則只要改其function即可 const getSum = (a: number, b: number) => a > b ? (a + b) : (a < b ? a - b : 0) const getSum2 = (sum: number, a: number, b: number) => sum > a ? sum + a : sum + b const calNumbers = (a: number, b: number, c?: number) => { const sum = getSum(a, b) const sum2 = getSum2(sum, a, b) return sum2 > c ? sum2 * c : sum2 } ``` --- CDD 與CDD簡單舉例(僅為個人習慣寫法的舉例): 以一個簡單的頁面component做範例 * 非CDD: 一旦Page component的header或是article需要更改或是根據情境可以替換,那麼就需要找到article相關的code並加以更改,維護上以及在測試都會比較不方便 * CDD: 將Page component的header或是article獨立拆分成個別的components,那麼需要更改該components時,只需要個別處理,且更改的部分(像是在storybook或其他呈現UI的工具)就會一目瞭然看出更改前後的差別! ```typescript= //not CDD const Page = () => { return ( <div> <header> <div>{'I am home page!'}</div> </header> <article> <h2>{'some title...'}</h2> <p>{'some contents...'}</p> </article> <footer> <ul> <li>{'about us...'}</li> <li>{'contact us...'}</li> </ul> </footer> </div> ) } //CDD const Header = () => { return ( <header> <div>{'I am home page!'}</div> </header> ) } const Article = () => { return ( <article> <h2>{'some title...'}</h2> <p>{'some contents...'}</p> </article> ) } const Footer = () => { return ( <footer> <ul> <li>{'about us...'}</li> <li>{'contact us...'}</li> </ul> </footer> ) } const Page = () => { return ( <div> <Header /> <Article /> <Footer /> </div> ) } ``` --- * ### 舊專案的 .js .jx -> .ts .tsx 遷移 js與ts最大差別在於types,因此原本寫的components/functions/class...都需要額外加上型別,雖然主要方法就如同前面所述的工作流的幾種方法(API, components, API & components), 但是其中最大的差別就是在於撰寫types及規劃types,這是JavaScript所沒有的,而在定義型別的時候,會發現某些看起來很完美的程式,一旦要嚴格定義型別(除了any),就會因此而更改或重構部分的程式碼,可能會有以下幾種情況(至少是我遇到的): --- #### 嚴格定義型別,有時會需要轉換格式(像是Number(), String(), ...等的方法) ```typescript= //.js: 原本寫法, 都是假設資料皆為string const getMessages = (id, content) => ( id + content ) type ID = string | number //結果資料其實會有number型別,那麼就要修改加上格式轉換的部分, //以避免其他consume此function時出現問題! const getMessages = (id: ID, content: string | number): string => ( id.toString() + content.toString() ) ``` --- #### 除了上述修改的部分,有些型別的定義特別嚴格,需要透過額外定義的方式來限制其型別 ```typescript= type Position = [number, number] const positionA = [0, 0]; //typeof positionA: number[] 並不相等於 Position: [number, number] ``` --- #### 上述提到的自訂types檔,在專案具有一定的規模時,建議以獨立資料夾管理其常用types (各自types沒有共用的話,放在各自檔案中就好) --- 1. 建好檔案 ``` |- src |- my-types |- allCommonTypes |- index.d.ts |- pageA |- index.d.ts |- pageB |- index.d.ts ... ``` --- 2. 設定tsconfig.json中的typeRoots(讓typescript知道你的自訂types) ```typescript= //tsconfig.json { "compilerOptions": { ..., "typeRoots": [ "./my-types" //這邊加上自訂types檔的路徑,typescript就會使用這些types ] } } ``` --- 3. 使用types ```typescript= import { ID } from 'allCommonTypes' //共用的 type Area = [number, number] //自己用的 ... ``` --- * ### generics 泛型: function for types 可以帶入參數的types,就是generics 泛型,適合用在某些types會根據情境改變其型別 像是以下範例 ```typescript= //generics type type CommonData<Data> = { //generics的參數,使用<>包起來 id: string data: Data } //使用1 type UserData = CommonData<{name: string}> //等同於 type UserData = { id: string data: { name: string } } //使用2 type ImageData = CommonData<{ src: string created_date: string }> //等同於 type ImageData = { id: string data: { src: string created_date: string } } ``` --- 除了一般的types,其實generics更多是用在function,讓function的型別更加彈性更好用! ```typescript= //沒用generics type A = (x: string) => string type B = (x: number) => number //用了之後 type GetData<DataType> = (x: DataType) => DataType type A = GetData<string> type B = GetData<number> ```