---
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>
```